/*-------------------------------------------------------------------------
 *
 * fe-exec.c
 *	  与将查询发送到后端相关的函数
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 *
 * IDENTIFICATION
 *	  src/interfaces/libpq/fe-exec.c
 *
 *-------------------------------------------------------------------------
 */
#include "postgres_fe.h"

#include <ctype.h>
#include <fcntl.h>
#include <limits.h>

#ifdef WIN32
#include "win32.h"
#else
#include <unistd.h>
#endif

#include "libpq-fe.h"
#include "libpq-int.h"
#include "mb/pg_wchar.h"

/* 保持与libpq-fe.h中ExecStatusType相同的顺序 */
char	   *const pgresStatus[] = {
	"PGRES_EMPTY_QUERY",
	"PGRES_COMMAND_OK",
	"PGRES_TUPLES_OK",
	"PGRES_COPY_OUT",
	"PGRES_COPY_IN",
	"PGRES_BAD_RESPONSE",
	"PGRES_NONFATAL_ERROR",
	"PGRES_FATAL_ERROR",
	"PGRES_COPY_BOTH",
	"PGRES_SINGLE_TUPLE",
	"PGRES_PIPELINE_SYNC",
	"PGRES_PIPELINE_ABORTED"
};

/* 如果我们无法完全创建PGresult则返回此结果 */
static const PGresult OOM_result = {
	.resultStatus = PGRES_FATAL_ERROR,
	.client_encoding = PG_SQL_ASCII,
	.errMsg = "out of memory\n",
};

/*
 * PQescapeString和PQescapeBytea所需的静态状态；初始化为
 * 导致向后兼容行为的值
 */
static int	static_client_encoding = PG_SQL_ASCII;
static bool static_std_strings = false;


static PGEvent *fc_dupEvents(PGEvent *fc_events, int fc_count, size_t *fc_memSize);
static bool fc_pqAddTuple(PGresult *fc_res, PGresAttValue *fc_tup,
					   const char **fc_errmsgp);
static int	fc_PQsendQueryInternal(PGconn *fc_conn, const char *fc_query, bool fc_newQuery);
static bool fc_PQsendQueryStart(PGconn *fc_conn, bool fc_newQuery);
static int	fc_PQsendQueryGuts(PGconn *fc_conn,
							const char *fc_command,
							const char *fc_stmtName,
							int fc_nParams,
							const Oid *fc_paramTypes,
							const char *const *fc_paramValues,
							const int *fc_paramLengths,
							const int *fc_paramFormats,
							int fc_resultFormat);
static void fc_parseInput(PGconn *fc_conn);
static PGresult *fc_getCopyResult(PGconn *fc_conn, ExecStatusType fc_copytype);
static bool fc_PQexecStart(PGconn *fc_conn);
static PGresult *fc_PQexecFinish(PGconn *fc_conn);
static int	fc_PQsendDescribe(PGconn *fc_conn, char fc_desc_type,
						   const char *fc_desc_target);
static int	fc_check_field_number(const PGresult *fc_res, int fc_field_num);
static void fc_pqPipelineProcessQueue(PGconn *fc_conn);
static int	fc_pqPipelineFlush(PGconn *fc_conn);


/* ----------------
 * PGresult 的空间管理。
 *
 * 以前，libpq 为查询返回的每个元组的每个字段做了单独的 malloc()。这非常昂贵——malloc/free 消耗了相当大的一部分应用程序运行时间。而且实际上没有必要单独跟踪各个字段，因为它们在释放 PGresult 时会一起被释放。因此，现在我们从 malloc 中获取大块存储空间，并在这些块内为查询数据分配空间，使用一个极为简单的分配器。这显著减少了 malloc/free 调用的数量，同时也避免了 malloc 存储区域的碎片化。
 * PGresult 结构本身仍然是单独 malloc 的。我们可以将其与第一个分配块结合起来，但这会浪费空间，因为在没有实际需要额外存储的常见情况下（即 SQL 命令没有返回元组）。
 *
 * 我们还单独为元组指针的顶级数组进行 malloc，因为我们需要能够通过 realloc 进行扩展，而我们的简单空间分配器无法有效处理这个问题。（很遗憾 FE/BE 协议没有事先告诉我们将返回多少元组。）
 * PGresult 的所有其他辅助存储都保存在大小为 PGRESULT_DATA_BLOCKSIZE 的 PGresult_data 块中。每个块开始时的开销仅仅是一个指向下一个块的链接（如果有的话）。空闲空间管理信息保存在拥有的 PGresult 中。
 * 因此，返回少量数据的查询将需要三次 malloc 调用：一次用于 PGresult，一次用于元组指针数组，以及一次 PGresult_data 块。
 *
 * 只有最近分配的 PGresult_data 块是可以添加更多内容的候选对象——在较旧的块中剩余的任何额外空间都被浪费了。我们可以更聪明一些，搜索整个链条，但这里的重点是简单和快速。典型的应用程序反正都不会长时间保留 PGresult，因此在其中浪费的一些空间并不是问题。
 *
 * 空间分配器的调优常数为：
 * PGRESULT_DATA_BLOCKSIZE：标准分配块的大小（以字节为单位）
 * PGRESULT_ALIGN_BOUNDARY：二进制数据的假定对齐要求
 * PGRESULT_SEP_ALLOC_THRESHOLD：大于此值的对象将获得单独的块，而不是被挤入常规分配块中。
 * 正确功能的要求是：
 * PGRESULT_ALIGN_BOUNDARY 必须是所有机器数据类型对齐要求的倍数。（目前这是从配置测试中设置的，因此应该自动没问题。）
 * PGRESULT_SEP_ALLOC_THRESHOLD + PGRESULT_BLOCK_OVERHEAD <=
 *			PGRESULT_DATA_BLOCKSIZE
 *		pqResultAlloc 假设小于阈值的对象将适合新的块中。
 * 块末尾浪费的空间可能多达 PGRESULT_SEP_ALLOC_THRESHOLD，因此不值得让它太大。
 * ----------------
 */

#define PGRESULT_DATA_BLOCKSIZE		2048
#define PGRESULT_ALIGN_BOUNDARY		MAXIMUM_ALIGNOF /* from configure */
#define PGRESULT_BLOCK_OVERHEAD		Max(sizeof(PGresult_data), PGRESULT_ALIGN_BOUNDARY)
#define PGRESULT_SEP_ALLOC_THRESHOLD	(PGRESULT_DATA_BLOCKSIZE / 2)


/*
 * PQmakeEmptyPGresult
 * 返回一个新分配的、初始化的 PGresult，带有给定的状态。
 * 如果 conn 不为 NULL 且状态指示有错误，则会复制 conn 的
 * errorMessage。此外，任何 PGEvents 也会从 conn 中复制。
 *
 * 注意：复制 conn 的 errorMessage 的逻辑现在是冗余的；
 * 没有内部调用者使用它。然而，这种行为是为外部调用者文档化的，
 * 所以我们最好保留它。
 */
PGresult * PQmakeEmptyPGresult(PGconn *fc_conn, ExecStatusType fc_status)
{
	PGresult   *fc_result;

	fc_result = (PGresult *) malloc(sizeof(PGresult));
	if (!fc_result)
		return NULL;

	fc_result->ntups = 0;
	fc_result->numAttributes = 0;
	fc_result->attDescs = NULL;
	fc_result->tuples = NULL;
	fc_result->tupArrSize = 0;
	fc_result->numParameters = 0;
	fc_result->paramDescs = NULL;
	fc_result->resultStatus = fc_status;
	fc_result->cmdStatus[0] = '\0';
	fc_result->binary = 0;
	fc_result->events = NULL;
	fc_result->nEvents = 0;
	fc_result->errMsg = NULL;
	fc_result->errFields = NULL;
	fc_result->errQuery = NULL;
	fc_result->null_field[0] = '\0';
	fc_result->curBlock = NULL;
	fc_result->curOffset = 0;
	fc_result->spaceLeft = 0;
	fc_result->memorySize = sizeof(PGresult);

	if (fc_conn)
	{
		/* 复制我们可能需要对 PGresult 进行操作的连接数据 */
		fc_result->noticeHooks = fc_conn->noticeHooks;
		fc_result->client_encoding = fc_conn->client_encoding;

		/* 考虑复制 conn 的 errorMessage */
		switch (fc_status)
		{
			case PGRES_EMPTY_QUERY:
			case PGRES_COMMAND_OK:
			case PGRES_TUPLES_OK:
			case PGRES_COPY_OUT:
			case PGRES_COPY_IN:
			case PGRES_COPY_BOTH:
			case PGRES_SINGLE_TUPLE:
				/* 非错误情况 */
				break;
			default:
				/* 我们故意不在这里使用或修改 errorReported */
				pqSetResultError(fc_result, &fc_conn->errorMessage, 0);
				break;
		}

		/* 最后复制事件；如果我们需要 PQclear，则结果必须是有效的 */
		if (fc_conn->nEvents > 0)
		{
			fc_result->events = fc_dupEvents(fc_conn->events, fc_conn->nEvents,
									   &fc_result->memorySize);
			if (!fc_result->events)
			{
				PQclear(fc_result);
				return NULL;
			}
			fc_result->nEvents = fc_conn->nEvents;
		}
	}
	else
	{
		/* 默认... */
		fc_result->noticeHooks.noticeRec = NULL;
		fc_result->noticeHooks.noticeRecArg = NULL;
		fc_result->noticeHooks.noticeProc = NULL;
		fc_result->noticeHooks.noticeProcArg = NULL;
		fc_result->client_encoding = PG_SQL_ASCII;
	}

	return fc_result;
}

/*
 * PQsetResultAttrs
 *
 * 为给定结果设置属性。如果提供的结果中已经包含属性， 
 * 则此函数失败。如果 numAttributes 为零或 attDescs 为 NULL，
 * 则调用会被忽略。如果函数失败，返回零。如果函数成功，
 * 返回非零值。
 */
int PQsetResultAttrs(PGresult *fc_res, int fc_numAttributes, PGresAttDesc *fc_attDescs)
{
	int			fc_i;

	/* 如果参数为 NULL 或 OOM_result，失败 */
	if (!fc_res || (const PGresult *) fc_res == &OOM_result)
		return false;

	/* 如果属性已经存在，则不能被覆盖。 */
	if (fc_res->numAttributes > 0)
		return false;

	/* 忽略无操作请求 */
	if (fc_numAttributes <= 0 || !fc_attDescs)
		return true;

	fc_res->attDescs = (PGresAttDesc *)
		PQresultAlloc(fc_res, fc_numAttributes * sizeof(PGresAttDesc));

	if (!fc_res->attDescs)
		return false;

	fc_res->numAttributes = fc_numAttributes;
	memcpy(fc_res->attDescs, fc_attDescs, fc_numAttributes * sizeof(PGresAttDesc));

	/* 深复制属性名称，并确定格式 */
	fc_res->binary = 1;
	for (fc_i = 0; fc_i < fc_res->numAttributes; fc_i++)
	{
		if (fc_res->attDescs[fc_i].name)
			fc_res->attDescs[fc_i].name = pqResultStrdup(fc_res, fc_res->attDescs[fc_i].name);
		else
			fc_res->attDescs[fc_i].name = fc_res->null_field;

		if (!fc_res->attDescs[fc_i].name)
			return false;

		if (fc_res->attDescs[fc_i].format == 0)
			fc_res->binary = 0;
	}

	return true;
}

/*
 * PQcopyResult
 *
 * 返回提供的 'src' PGresult 的深复制，不能为 NULL。
 * 'flags' 参数控制结果的哪些部分将被复制或不复制。
 * 创建的结果始终处于 PGRES_TUPLES_OK 状态。源结果的错误信息不被复制，
 * 尽管 cmdStatus 是。
 *
 * 要设置自定义属性，请使用 PQsetResultAttrs。该函数要求结果中
 * 不包含属性，因此要使用该函数，您不能使用 PG_COPYRES_ATTRS
 * 或 PG_COPYRES_TUPLES 选项与此函数一起使用。
 *
 * 选项：
 *	 PG_COPYRES_ATTRS - 复制源结果的属性
 *
 *	 PG_COPYRES_TUPLES - 复制源结果的元组。这意味着
 *	 复制属性，因为元组需要属性。
 *
 *	 PG_COPYRES_EVENTS - 复制源结果的事件。
 *
 *	 PG_COPYRES_NOTICEHOOKS - 复制源结果的通知钩子。
 */
PGresult * PQcopyResult(const PGresult *fc_src, int fc_flags)
{
	PGresult   *fc_dest;
	int			fc_i;

	if (!fc_src)
		return NULL;

	fc_dest = PQmakeEmptyPGresult(NULL, PGRES_TUPLES_OK);
	if (!fc_dest)
		return NULL;

	/* 始终复制这些。cmdStatus 在这里真有用吗？ */
	fc_dest->client_encoding = fc_src->client_encoding;
	strcpy(fc_dest->cmdStatus, fc_src->cmdStatus);

	/* 想要属性吗？ */
	if (fc_flags & (PG_COPYRES_ATTRS | PG_COPYRES_TUPLES))
	{
		if (!PQsetResultAttrs(fc_dest, fc_src->numAttributes, fc_src->attDescs))
		{
			PQclear(fc_dest);
			return NULL;
		}
	}

	/* 想要复制元组吗？ */
	if (fc_flags & PG_COPYRES_TUPLES)
	{
		int			fc_tup,
					fc_field;

		for (fc_tup = 0; fc_tup < fc_src->ntups; fc_tup++)
		{
			for (fc_field = 0; fc_field < fc_src->numAttributes; fc_field++)
			{
				if (!PQsetvalue(fc_dest, fc_tup, fc_field,
								fc_src->tuples[fc_tup][fc_field].value,
								fc_src->tuples[fc_tup][fc_field].len))
				{
					PQclear(fc_dest);
					return NULL;
				}
			}
		}
	}

	/* 想要复制通知钩子吗？ */
	if (fc_flags & PG_COPYRES_NOTICEHOOKS)
		fc_dest->noticeHooks = fc_src->noticeHooks;

	/* 想要复制 PGEvents 吗？ */
	if ((fc_flags & PG_COPYRES_EVENTS) && fc_src->nEvents > 0)
	{
		fc_dest->events = fc_dupEvents(fc_src->events, fc_src->nEvents,
								 &fc_dest->memorySize);
		if (!fc_dest->events)
		{
			PQclear(fc_dest);
			return NULL;
		}
		fc_dest->nEvents = fc_src->nEvents;
	}

	/* 好吧，触发 PGEVT_RESULTCOPY 事件 */
	for (fc_i = 0; fc_i < fc_dest->nEvents; fc_i++)
	{
		/* 对于之前发生某些失败的事件，我们不会触发 */
		if (fc_src->events[fc_i].resultInitialized)
		{
			PGEventResultCopy fc_evt;

			fc_evt.src = fc_src;
			fc_evt.dest = fc_dest;
			if (fc_dest->events[fc_i].proc(PGEVT_RESULTCOPY, &fc_evt,
									 fc_dest->events[fc_i].passThrough))
				fc_dest->events[fc_i].resultInitialized = true;
		}
	}

	return fc_dest;
}

/*
 * 复制一个 PGEvents 数组（没有额外空间用于更多）。
 * 不重复事件实例数据，将其设置为 NULL。
 * 此外，结果初始化标志全部清除。
 * 分配的总空间被添加到 *memSize。
 */
static PGEvent *
fc_dupEvents(PGEvent *fc_events, int fc_count, size_t *fc_memSize)
{
	PGEvent    *fc_newEvents;
	size_t		fc_msize;
	int			fc_i;

	if (!fc_events || fc_count <= 0)
		return NULL;

	fc_msize = fc_count * sizeof(PGEvent);
	fc_newEvents = (PGEvent *) malloc(fc_msize);
	if (!fc_newEvents)
		return NULL;

	for (fc_i = 0; fc_i < fc_count; fc_i++)
	{
		fc_newEvents[fc_i].proc = fc_events[fc_i].proc;
		fc_newEvents[fc_i].passThrough = fc_events[fc_i].passThrough;
		fc_newEvents[fc_i].data = NULL;
		fc_newEvents[fc_i].resultInitialized = false;
		fc_newEvents[fc_i].name = strdup(fc_events[fc_i].name);
		if (!fc_newEvents[fc_i].name)
		{
			while (--fc_i >= 0)
				free(fc_newEvents[fc_i].name);
			free(fc_newEvents);
			return NULL;
		}
		fc_msize += strlen(fc_events[fc_i].name) + 1;
	}

	*fc_memSize += fc_msize;
	return fc_newEvents;
}


/*
 * 设置元组字段的值。tup_num 必须小于或等于 PQntuples(res)。如果相等，则会创建一个新元组并添加到结果中。
 * 成功时返回非零值，失败时返回零。
 * （在失败时，我们通过 pqInternalNotice 报告具体问题。）
 */
int PQsetvalue(PGresult *fc_res, int fc_tup_num, int fc_field_num, char *fc_value, int fc_len)
{
	PGresAttValue *fc_attval;
	const char *fc_errmsg = NULL;

	/* 如果参数为 NULL 或 OOM_result，失败 */
	if (!fc_res || (const PGresult *) fc_res == &OOM_result)
		return false;

	/* 无效的 field_num? */
	if (!fc_check_field_number(fc_res, fc_field_num))
		return false;

	/* 无效的 tup_num，必须 <= ntups */
	if (fc_tup_num < 0 || fc_tup_num > fc_res->ntups)
	{
		pqInternalNotice(&fc_res->noticeHooks,
						 "row number %d is out of range 0..%d",
						 fc_tup_num, fc_res->ntups);
		return false;
	}

	/* 需要分配一个新元组吗？ */
	if (fc_tup_num == fc_res->ntups)
	{
		PGresAttValue *fc_tup;
		int			fc_i;

		fc_tup = (PGresAttValue *)
			pqResultAlloc(fc_res, fc_res->numAttributes * sizeof(PGresAttValue),
						  true);

		if (!fc_tup)
			goto fail;

		/* 将每一列初始化为 NULL */
		for (fc_i = 0; fc_i < fc_res->numAttributes; fc_i++)
		{
			fc_tup[fc_i].len = NULL_LEN;
			fc_tup[fc_i].value = fc_res->null_field;
		}

		/* 将其添加到数组中 */
		if (!fc_pqAddTuple(fc_res, fc_tup, &fc_errmsg))
			goto fail;
	}

	fc_attval = &fc_res->tuples[fc_tup_num][fc_field_num];

	/* 将 NULL_LEN 或 NULL 值指针视为 NULL 字段 */
	if (fc_len == NULL_LEN || fc_value == NULL)
	{
		fc_attval->len = NULL_LEN;
		fc_attval->value = fc_res->null_field;
	}
	else if (fc_len <= 0)
	{
		fc_attval->len = 0;
		fc_attval->value = fc_res->null_field;
	}
	else
	{
		fc_attval->value = (char *) pqResultAlloc(fc_res, fc_len + 1, true);
		if (!fc_attval->value)
			goto fail;
		fc_attval->len = fc_len;
		memcpy(fc_attval->value, fc_value, fc_len);
		fc_attval->value[fc_len] = '\0';
	}

	return true;

	/*
	 * 通过 pqInternalNotice 报告失败。如果前面的代码没有提供
	 * 错误信息，则假定“内存不足”是指的。
	 */
fail:
	if (!fc_errmsg)
		fc_errmsg = libpq_gettext("out of memory");
	pqInternalNotice(&fc_res->noticeHooks, "%s", fc_errmsg);

	return false;
}

/*
 * pqResultAlloc - 导出例程，用于在 PGresult 中分配本地存储。
 *
 * 我们强制所有这些分配都要按最大对齐，因为我们不知道
 * 值可能是二进制的。
 */
void * PQresultAlloc(PGresult *fc_res, size_t fc_nBytes)
{
	/* 如果参数为 NULL 或 OOM_result，失败 */
	if (!fc_res || (const PGresult *) fc_res == &OOM_result)
		return NULL;

	return pqResultAlloc(fc_res, fc_nBytes, true);
}

/*
 * pqResultAlloc -
 *		为 PGresult 分配附属存储。
 *
 * nBytes 是对象所需的空间量。
 * 如果 isBinary 为真，我们假定需要在机器分配边界上对齐对象。
 * 如果 isBinary 为假，我们假定对象是字符字符串，可以
 * 在任何字节边界上分配。
 */
void * pqResultAlloc(PGresult *fc_res, size_t fc_nBytes, bool fc_isBinary)
{
	char	   *fc_space;
	PGresult_data *fc_block;

	if (!fc_res)
		return NULL;

	if (fc_nBytes <= 0)
		return fc_res->null_field;

	/*
	 * 如果需要对齐，将当前的位置向上取整到对齐
	 * 边界。
	 */
	if (fc_isBinary)
	{
		int			fc_offset = fc_res->curOffset % PGRESULT_ALIGN_BOUNDARY;

		if (fc_offset)
		{
			fc_res->curOffset += PGRESULT_ALIGN_BOUNDARY - fc_offset;
			fc_res->spaceLeft -= PGRESULT_ALIGN_BOUNDARY - fc_offset;
		}
	}

	/* 如果当前块中有足够的空间，没问题。 */
	if (fc_nBytes <= (size_t) fc_res->spaceLeft)
	{
		fc_space = fc_res->curBlock->space + fc_res->curOffset;
		fc_res->curOffset += fc_nBytes;
		fc_res->spaceLeft -= fc_nBytes;
		return fc_space;
	}

	/*
	 * 如果请求的对象非常大，为其提供自己的块；这
	 * 避免了将大部分当前块浪费在启动新块上。（无论如何，我们
	 * 对于大于块大小的请求必须进行特殊处理。）在这种情况下，
	 * 对象始终会获得二进制对齐。
	 */
	if (fc_nBytes >= PGRESULT_SEP_ALLOC_THRESHOLD)
	{
		size_t		fc_alloc_size = fc_nBytes + PGRESULT_BLOCK_OVERHEAD;

		fc_block = (PGresult_data *) malloc(fc_alloc_size);
		if (!fc_block)
			return NULL;
		fc_res->memorySize += fc_alloc_size;
		fc_space = fc_block->space + PGRESULT_BLOCK_OVERHEAD;
		if (fc_res->curBlock)
		{
			/*
			 * 将特殊块放置在活动块下方，以便我们不必
			 * 浪费活动块中的空闲空间。
			 */
			fc_block->next = fc_res->curBlock->next;
			fc_res->curBlock->next = fc_block;
		}
		else
		{
			/* 必须将新块设置为第一个活动块。 */
			fc_block->next = NULL;
			fc_res->curBlock = fc_block;
			fc_res->spaceLeft = 0; /* 确保它被标记为满 */
		}
		return fc_space;
	}

	/* 否则，开始一个新块。 */
	fc_block = (PGresult_data *) malloc(PGRESULT_DATA_BLOCKSIZE);
	if (!fc_block)
		return NULL;
	fc_res->memorySize += PGRESULT_DATA_BLOCKSIZE;
	fc_block->next = fc_res->curBlock;
	fc_res->curBlock = fc_block;
	if (fc_isBinary)
	{
		/* 对象需要完全对齐 */
		fc_res->curOffset = PGRESULT_BLOCK_OVERHEAD;
		fc_res->spaceLeft = PGRESULT_DATA_BLOCKSIZE - PGRESULT_BLOCK_OVERHEAD;
	}
	else
	{
		/* 我们可以将其紧贴在开销指针后面 */
		fc_res->curOffset = sizeof(PGresult_data);
		fc_res->spaceLeft = PGRESULT_DATA_BLOCKSIZE - sizeof(PGresult_data);
	}

	fc_space = fc_block->space + fc_res->curOffset;
	fc_res->curOffset += fc_nBytes;
	fc_res->spaceLeft -= fc_nBytes;
	return fc_space;
}

/*
 * PQresultMemorySize -
 *		返回为 PGresult 分配的总空间。
 */
size_t PQresultMemorySize(const PGresult *fc_res)
{
	if (!fc_res)
		return 0;
	return fc_res->memorySize;
}

/*
 * pqResultStrdup -
 *		类似 strdup，但空间是附属 PGresult 空间。
 */
char * pqResultStrdup(PGresult *fc_res, const char *fc_str)
{
	char	   *fc_space = (char *) pqResultAlloc(fc_res, strlen(fc_str) + 1, false);

	if (fc_space)
		strcpy(fc_space, fc_str);
	return fc_space;
}

/*
 * pqSetResultError -
 *		为 PGresult 分配一条新的错误信息
 *
 * 从 errorMessage 缓冲区复制文本，开始于给定的偏移量
 * （由调用者负责偏移量的有效性）
 */
void pqSetResultError(PGresult *fc_res, PQExpBuffer fc_errorMessage, int fc_offset)
{
	char	   *fc_msg;

	if (!fc_res)
		return;

	/*
	 * 我们在这里处理两种 OOM 情况。errorMessage 缓冲区可能被
	 * 标记为“损坏”，因为之前未能为消息分配足够的内存，或者它可能是好的，但 pqResultStrdup 失败
	 * 并返回 NULL。在这两种情况下，只需使 res->errMsg 直接
	 * 指向一个常量“内存不足”字符串。
	 */
	if (!PQExpBufferBroken(fc_errorMessage))
		fc_msg = pqResultStrdup(fc_res, fc_errorMessage->data + fc_offset);
	else
		fc_msg = NULL;
	if (fc_msg)
		fc_res->errMsg = fc_msg;
	else
		fc_res->errMsg = libpq_gettext("out of memory\n");
}

/*
 * PQclear -
 *	  释放与 PGresult 相关的内存
 */
void PQclear(PGresult *fc_res)
{
	PGresult_data *fc_block;
	int			fc_i;

	/* 作为便捷，对于 NULL 指针不做任何操作 */
	if (!fc_res)
		return;
	/* 如果参数是 OOM_result，也不做任何操作 */
	if ((const PGresult *) fc_res == &OOM_result)
		return;

	/* 关闭我们可能拥有的任何事件 */
	for (fc_i = 0; fc_i < fc_res->nEvents; fc_i++)
	{
		/* 仅向成功初始化的事件处理程序发送 DESTROY */
		if (fc_res->events[fc_i].resultInitialized)
		{
			PGEventResultDestroy fc_evt;

			fc_evt.result = fc_res;
			(void) fc_res->events[fc_i].proc(PGEVT_RESULTDESTROY, &fc_evt,
									   fc_res->events[fc_i].passThrough);
		}
		free(fc_res->events[fc_i].name);
	}

	if (fc_res->events)
		free(fc_res->events);

	/* 释放所有附属块 */
	while ((fc_block = fc_res->curBlock) != NULL)
	{
		fc_res->curBlock = fc_block->next;
		free(fc_block);
	}

	/* 释放顶层元组指针数组 */
	if (fc_res->tuples)
		free(fc_res->tuples);

	/* 将指针字段清零以捕捉编程错误 */
	fc_res->attDescs = NULL;
	fc_res->tuples = NULL;
	fc_res->paramDescs = NULL;
	fc_res->errFields = NULL;
	fc_res->events = NULL;
	fc_res->nEvents = 0;
	/* res->curBlock 之前已被清零 */

	/* 释放 PGresult 结构本身 */
	free(fc_res);
}

/*
 * 方便的子例程，用于释放任何部分构建的异步结果。
 *
 * 任何 "下一个" 结果也会被清除。
 */
void pqClearAsyncResult(PGconn *fc_conn)
{
	if (fc_conn->result)
		PQclear(fc_conn->result);
	fc_conn->result = NULL;
	fc_conn->error_result = false;
	if (fc_conn->next_result)
		PQclear(fc_conn->next_result);
	fc_conn->next_result = NULL;
}

/*
 * pqSaveErrorResult -
 *	  记录我们有一个错误条件
 *
 * 在 libpq 的大部分中，报告错误只需要将文本附加到
 * conn->errorMessage 并向调用者返回一个失败代码。
 * 在返回失败代码不切实际的地方，改为调用此
 * 函数以记住需要报告一个错误。
 *
 * （看起来将文本附加到 conn->errorMessage 应该
 * 足够，但我们不能依赖在内存不足的情况下
 * 也能正常工作。内存不足的危险也是我们不尝试在这里
 * 创建新的 PGresult 的原因。）
 */
void pqSaveErrorResult(PGconn *fc_conn)
{
	/* 丢弃任何待处理的结果... */
	pqClearAsyncResult(fc_conn);
	/* ... 并设置标志以记住稍后制作错误结果 */
	fc_conn->error_result = true;
}

/*
 * pqSaveWriteError -
 *	  报告写入失败
 *
 * 如上所述，在将 conn->write_err_msg 附加到我们
 * 拥有的任何其他错误之后。这在我们检测到写入
 * 失败并且已经耗尽报告其他内容的机会时使用。
 */
static void fc_pqSaveWriteError(PGconn *fc_conn)
{
	/*
	 * 如果 write_err_msg 因之前的 strdup 失败而为空，尽我们
	 * 所能做的事情。（我们的操作可能也会导致内存不足
	 * 的失败，但还是尝试一下。）
	 */
	if (fc_conn->write_err_msg)
	{
		appendPQExpBufferStr(&fc_conn->errorMessage, fc_conn->write_err_msg);
		/* 避免可能重复附加相同的消息 */
		fc_conn->write_err_msg[0] = '\0';
	}
	else
		appendPQExpBufferStr(&fc_conn->errorMessage,
							 libpq_gettext("write to server failed\n"));

	pqSaveErrorResult(fc_conn);
}

/*
 * pqPrepareAsyncResult -
 *	  准备当前异步结果对象以返回给调用者
 *
 * 如果尚不存在异步结果对象，使用 conn->errorMessage
 * 中的内容构建一个错误对象。在任何情况下，都要清除
 * 异步结果存储，并更新我们对已返回给应用程序的
 * 错误文本量的概念。
 *
 * 请注意，在任何情况下（甚至内存不足的情况）
 * 我们都不会返回 NULL。
 */
PGresult * pqPrepareAsyncResult(PGconn *fc_conn)
{
	PGresult   *fc_res;

	fc_res = fc_conn->result;
	if (fc_res)
	{
		/*
		 * 如果现有结果是 ERROR（大概是从服务器接收到的
		 * 内容），假定它代表 conn->errorMessage 中的内容，并
		 * 前进 errorReported。
		 */
		if (fc_res->resultStatus == PGRES_FATAL_ERROR)
			fc_conn->errorReported = fc_conn->errorMessage.len;
	}
	else
	{
		/*
		 * 我们在 libpq 内部错误之后到达这里。我们可能应该
		 * 始终有 error_result = true，但如果没有，生成一些
		 * 错误文本。
		 */
		if (!fc_conn->error_result)
			appendPQExpBufferStr(&fc_conn->errorMessage,
								 libpq_gettext("no error text available\n"));

		/* 妄想：确保 errorReported 偏移量是合理的 */
		if (fc_conn->errorReported < 0 ||
			fc_conn->errorReported >= fc_conn->errorMessage.len)
			fc_conn->errorReported = 0;

		/*
		 * 为错误创建一个 PGresult 结构。我们暂时对结果
		 * 状态撒谎，以便 PQmakeEmptyPGresult 不会无用地复制
		 * 所有 conn->errorMessage。
		 */
		fc_res = PQmakeEmptyPGresult(fc_conn, PGRES_EMPTY_QUERY);
		if (fc_res)
		{
			/*
			 * 报告我们拥有的任何新的错误文本，并推进
			 * errorReported。
			 */
			fc_res->resultStatus = PGRES_FATAL_ERROR;
			pqSetResultError(fc_res, &fc_conn->errorMessage, fc_conn->errorReported);
			fc_conn->errorReported = fc_conn->errorMessage.len;
		}
		else
		{
			/*
			 * 哎呀，PGresult的内存不足。幸运的是，我们有一张
			 * 王牌：我们可以使用静态的OOM_result。在这里
			 * 强制转换const有点丑陋，但最好将OOM_result声明为const，
			 * 希望它会被分配在只读存储中。
			 */
			fc_res = unconstify(PGresult *, &OOM_result);

			/*
			 * 不要推进errorReported。也许我们稍后能够报告
			 * 文本。
			 */
		}
	}

	/*
	 * 如果有的话，用next_result替换conn->result。在正常情况下
	 * 没有下一个结果，我们只是放弃当前结果的所有权。在单行模式下，
	 * 这将情况还原为我们创建当前单行结果之前的状态。
	 */
	fc_conn->result = fc_conn->next_result;
	fc_conn->error_result = false; /* next_result永远不会是错误 */
	fc_conn->next_result = NULL;

	return fc_res;
}

/*
 * pqInternalNotice - 生成一条内部生成的通知消息
 *
 * 可以传递格式字符串和可选参数。请注意，我们在这里调用
 * libpq_gettext()，因此调用方不需要这样做。
 *
 * 提供的文本被视为主要消息（即，它不应包括
 * 结尾换行符，并且不应超过一行）。
 */
void pqInternalNotice(const PGNoticeHooks *fc_hooks, const char *fc_fmt,...)
{
	char		fc_msgBuf[1024];
	va_list		fc_args;
	PGresult   *fc_res;

	if (fc_hooks->noticeRec == NULL)
		return;					/* 没有人来接收通知？ */

	/* 格式化消息 */
	va_start(fc_args, fc_fmt);
	vsnprintf(fc_msgBuf, sizeof(fc_msgBuf), libpq_gettext(fc_fmt), fc_args);
	va_end(fc_args);
	fc_msgBuf[sizeof(fc_msgBuf) - 1] = '\0';	/* 确保它以正确结束 */

	/* 创建一个PGresult以传递给通知接收器 */
	fc_res = PQmakeEmptyPGresult(NULL, PGRES_NONFATAL_ERROR);
	if (!fc_res)
		return;
	fc_res->noticeHooks = *fc_hooks;

	/*
	 * 设置通知的字段。
	 */
	pqSaveMessageField(fc_res, PG_DIAG_MESSAGE_PRIMARY, fc_msgBuf);
	pqSaveMessageField(fc_res, PG_DIAG_SEVERITY, libpq_gettext("NOTICE"));
	pqSaveMessageField(fc_res, PG_DIAG_SEVERITY_NONLOCALIZED, "NOTICE");
	/* XXX 是否也应该提供SQLSTATE？ */

	/*
	 * 结果文本始终只是主要消息 + 换行符。如果我们无法
	 * 分配它，则替换为“内存不足”，如pqSetResultError中所示。
	 */
	fc_res->errMsg = (char *) pqResultAlloc(fc_res, strlen(fc_msgBuf) + 2, false);
	if (fc_res->errMsg)
		sprintf(fc_res->errMsg, "%s\n", fc_msgBuf);
	else
		fc_res->errMsg = libpq_gettext("out of memory\n");

	/*
	 * 传递给接收器，然后释放它。
	 */
	fc_res->noticeHooks.noticeRec(fc_res->noticeHooks.noticeRecArg, fc_res);
	PQclear(fc_res);
}

/*
 * pqAddTuple
 *	  向PGresult结构添加行指针，如有必要则扩大它
 *	  如果可以则返回true，如果由于错误无法添加行，则返回false
 *
 * 出错时，可以将*errmsgp设置为要返回的错误字符串。
 * 如果将其设置为NULL，则假定错误为“内存不足”。
 */
static bool fc_pqAddTuple(PGresult *fc_res, PGresAttValue *fc_tup, const char **fc_errmsgp)
{
	if (fc_res->ntups >= fc_res->tupArrSize)
	{
		/*
		 * 尝试扩展数组。
		 *
		 * 我们可以使用realloc，因为对结构的浅拷贝是
		 * 可以的。请注意，第一次通过时，res->tuples为NULL。
		 * ANSI表示，在这种情况下，realloc()应该表现得像malloc()，
		 * 但一些旧的C库（如SunOS 4.1.x）会导致核心转储。
		 * 失败时，realloc应该返回NULL而不损坏
		 * 现有的分配。请注意，res->ntups以后的位置是
		 * 垃圾，不一定是NULL。
		 */
		int			fc_newSize;
		PGresAttValue **fc_newTuples;

		/*
		 * 由于我们使用整数作为行号，因此无法支持超过
		 * INT_MAX行。确保我们允许这么多行。
		 */
		if (fc_res->tupArrSize <= INT_MAX / 2)
			fc_newSize = (fc_res->tupArrSize > 0) ? fc_res->tupArrSize * 2 : 128;
		else if (fc_res->tupArrSize < INT_MAX)
			fc_newSize = INT_MAX;
		else
		{
			*fc_errmsgp = libpq_gettext("PGresult cannot support more than INT_MAX tuples");
			return false;
		}

		/*
		 * 此外，在32位平台上，我们理论上甚至可能在
		 * newSize达到INT_MAX之前就溢出size_t。（实际上，我们无疑会在
		 * 那之前很久就遇到OOM，但让我们检查一下。）
		 */
#if INT_MAX >= (SIZE_MAX / 2)
		if (newSize > SIZE_MAX / sizeof(PGresAttValue *))
		{
			*errmsgp = libpq_gettext("size_t overflow");
			return false;
		}
#endif

		if (fc_res->tuples == NULL)
			fc_newTuples = (PGresAttValue **)
				malloc(fc_newSize * sizeof(PGresAttValue *));
		else
			fc_newTuples = (PGresAttValue **)
				realloc(fc_res->tuples, fc_newSize * sizeof(PGresAttValue *));
		if (!fc_newTuples)
			return false;		/* malloc或realloc失败 */
		fc_res->memorySize +=
			(fc_newSize - fc_res->tupArrSize) * sizeof(PGresAttValue *);
		fc_res->tupArrSize = fc_newSize;
		fc_res->tuples = fc_newTuples;
	}
	fc_res->tuples[fc_res->ntups] = fc_tup;
	fc_res->ntups++;
	return true;
}

/*
 * pqSaveMessageField - 保存错误或通知消息的一个字段
 */
void pqSaveMessageField(PGresult *fc_res, char fc_code, const char *fc_value)
{
	PGMessageField *fc_pfield;

	fc_pfield = (PGMessageField *)
		pqResultAlloc(fc_res,
					  offsetof(PGMessageField, contents) +
					  strlen(fc_value) + 1,
					  true);
	if (!fc_pfield)
		return;					/* 内存不足？ */
	fc_pfield->code = fc_code;
	strcpy(fc_pfield->contents, fc_value);
	fc_pfield->next = fc_res->errFields;
	fc_res->errFields = fc_pfield;
}

/*
 * pqSaveParameterStatus - 记住由后端发送的参数状态
 */
void pqSaveParameterStatus(PGconn *fc_conn, const char *fc_name, const char *fc_value)
{
	pgParameterStatus *fc_pstatus;
	pgParameterStatus *fc_prev;

	/*
	 * 忘记关于该参数的任何旧信息
	 */
	for (fc_pstatus = fc_conn->pstatus, fc_prev = NULL;
		 fc_pstatus != NULL;
		 fc_prev = fc_pstatus, fc_pstatus = fc_pstatus->next)
	{
		if (strcmp(fc_pstatus->name, fc_name) == 0)
		{
			if (fc_prev)
				fc_prev->next = fc_pstatus->next;
			else
				fc_conn->pstatus = fc_pstatus->next;
			free(fc_pstatus);		/* 也释放名称和值字符串 */
			break;
		}
	}

	/*
	 * 将新信息作为单个malloc块存储
	 */
	fc_pstatus = (pgParameterStatus *) malloc(sizeof(pgParameterStatus) +
										   strlen(fc_name) + strlen(fc_value) + 2);
	if (fc_pstatus)
	{
		char	   *fc_ptr;

		fc_ptr = ((char *) fc_pstatus) + sizeof(pgParameterStatus);
		fc_pstatus->name = fc_ptr;
		strcpy(fc_ptr, fc_name);
		fc_ptr += strlen(fc_name) + 1;
		fc_pstatus->value = fc_ptr;
		strcpy(fc_ptr, fc_value);
		fc_pstatus->next = fc_conn->pstatus;
		fc_conn->pstatus = fc_pstatus;
	}

	/*
	 * 保存libpq感兴趣的设置值到PGconn对象的字段中。
	 * 我们还将client_encoding和standard_conforming_strings保存在静态变量中，
	 * 以便PQescapeString和PQescapeBytea可以表现得相对合理（至少在使用单连接的程序中）。
	 */
	if (strcmp(fc_name, "client_encoding") == 0)
	{
		fc_conn->client_encoding = pg_char_to_encoding(fc_value);
		/* 如果我们不认识编码名称，则回退到SQL_ASCII */
		if (fc_conn->client_encoding < 0)
			fc_conn->client_encoding = PG_SQL_ASCII;
		static_client_encoding = fc_conn->client_encoding;
	}
	else if (strcmp(fc_name, "standard_conforming_strings") == 0)
	{
		fc_conn->std_strings = (strcmp(fc_value, "on") == 0);
		static_std_strings = fc_conn->std_strings;
	}
	else if (strcmp(fc_name, "server_version") == 0)
	{
		/* 我们将服务器版本转换为数值形式。 */
		int			fc_cnt;
		int			fc_vmaj,
					fc_vmin,
					fc_vrev;

		fc_cnt = sscanf(fc_value, "%d.%d.%d", &fc_vmaj, &fc_vmin, &fc_vrev);

		if (fc_cnt == 3)
		{
			/* 旧样式，例如9.6.1 */
			fc_conn->sversion = (100 * fc_vmaj + fc_vmin) * 100 + fc_vrev;
		}
		else if (fc_cnt == 2)
		{
			if (fc_vmaj >= 10)
			{
				/* 新样式，例如10.1 */
				fc_conn->sversion = 100 * 100 * fc_vmaj + fc_vmin;
			}
			else
			{
				/* 没有小版本的旧样式，例如9.6devel */
				fc_conn->sversion = (100 * fc_vmaj + fc_vmin) * 100;
			}
		}
		else if (fc_cnt == 1)
		{
			/* 没有小版本的新样式，例如10devel */
			fc_conn->sversion = 100 * 100 * fc_vmaj;
		}
		else
			fc_conn->sversion = 0; /* 未知 */
	}
	else if (strcmp(fc_name, "default_transaction_read_only") == 0)
	{
		fc_conn->default_transaction_read_only =
			(strcmp(fc_value, "on") == 0) ? PG_BOOL_YES : PG_BOOL_NO;
	}
	else if (strcmp(fc_name, "in_hot_standby") == 0)
	{
		fc_conn->in_hot_standby =
			(strcmp(fc_value, "on") == 0) ? PG_BOOL_YES : PG_BOOL_NO;
	}
}


/*
 * pqRowProcessor
 *	  将接收到的行添加到当前异步结果(conn->result)中。
 *	  如果正常，返回1；如果发生错误，返回0。
 *
 * 在错误情况下，可以将*errmsgp设置为要返回的错误字符串。
 * （这样的字符串应该通过libpq_gettext()进行翻译。）
 * 如果留空NULL，则假定错误为“内存不足”。
 *
 * 在单行模式下，我们创建一个新的结果，仅保存当前行，
 * 将之前的结果存储在conn->next_result中，以便在pqPrepareAsyncResult()之后
 * 重新激活。这样可以将结果元数据（列描述）传递到每个结果行。
 */
int pqRowProcessor(PGconn *fc_conn, const char **fc_errmsgp)
{
	PGresult   *fc_res = fc_conn->result;
	int			fc_nfields = fc_res->numAttributes;
	const PGdataValue *fc_columns = fc_conn->rowBuf;
	PGresAttValue *fc_tup;
	int			fc_i;

	/*
	 * 在单行模式下，创建一个新的PGresult，仅保存这一行；
	 * 原始的conn->result保持不变，以便将其用作未来行的模板。
	 */
	if (fc_conn->singleRowMode)
	{
		/* 复制此时结果中应包含的所有内容 */
		fc_res = PQcopyResult(fc_res,
						   PG_COPYRES_ATTRS | PG_COPYRES_EVENTS |
						   PG_COPYRES_NOTICEHOOKS);
		if (!fc_res)
			return 0;
	}

	/*
	 * 基本上，我们仅在PGresult中为每个字段分配空间并复制数据。
	 *
	 * 注意：在malloc失败时，我们返回0，留下*errmsgp仍为NULL，
	 * 调用者将其理解为“内存不足”。这比在这里尝试设置这样的消息更可取，
	 * 因为显然没有足够的内存让gettext()做任何事情。
	 */
	fc_tup = (PGresAttValue *)
		pqResultAlloc(fc_res, fc_nfields * sizeof(PGresAttValue), true);
	if (fc_tup == NULL)
		goto fail;

	for (fc_i = 0; fc_i < fc_nfields; fc_i++)
	{
		int			fc_clen = fc_columns[fc_i].len;

		if (fc_clen < 0)
		{
			/* 空字段 */
			fc_tup[fc_i].len = NULL_LEN;
			fc_tup[fc_i].value = fc_res->null_field;
		}
		else
		{
			bool		fc_isbinary = (fc_res->attDescs[fc_i].format != 0);
			char	   *fc_val;

			fc_val = (char *) pqResultAlloc(fc_res, fc_clen + 1, fc_isbinary);
			if (fc_val == NULL)
				goto fail;

			/* 复制并以零终止数据（即使它是二进制的） */
			memcpy(fc_val, fc_columns[fc_i].value, fc_clen);
			fc_val[fc_clen] = '\0';

			fc_tup[fc_i].len = fc_clen;
			fc_tup[fc_i].value = fc_val;
		}
	}

	/* 并将元组添加到PGresult的元组数组中 */
	if (!fc_pqAddTuple(fc_res, fc_tup, fc_errmsgp))
		goto fail;

	/*
	 * 成功。在单行模式下，立即将结果提供给客户端。
	 */
	if (fc_conn->singleRowMode)
	{
		/* 将结果状态更改为特殊的单行值 */
		fc_res->resultStatus = PGRES_SINGLE_TUPLE;
		/* 存储旧结果以便后续重用 */
		fc_conn->next_result = fc_conn->result;
		fc_conn->result = fc_res;
		/* 并标记结果准备好返回 */
		fc_conn->asyncStatus = PGASYNC_READY_MORE;
	}

	return 1;

fail:
	/* 释放本地分配的PGresult（如果我们创建了一个） */
	if (fc_res != fc_conn->result)
		PQclear(fc_res);
	return 0;
}


/*
 * pqAllocCmdQueueEntry
 *		为调用者获取一个命令队列条目以填充。
 *
 * 如果回收队列中有空闲元素，则返回该元素；如果没有，则
 * 分配一个新的。调用者必须在结构填充后将其添加到
 * 命令队列（pqAppendCmdQueueEntry），或者在发生错误时
 * 释放内存（pqRecycleCmdQueueEntry）。
 *
 * 如果分配失败，设置错误消息并返回 NULL。
 */
static PGcmdQueueEntry *
fc_pqAllocCmdQueueEntry(PGconn *fc_conn)
{
	PGcmdQueueEntry *fc_entry;

	if (fc_conn->cmd_queue_recycle == NULL)
	{
		fc_entry = (PGcmdQueueEntry *) malloc(sizeof(PGcmdQueueEntry));
		if (fc_entry == NULL)
		{
			appendPQExpBufferStr(&fc_conn->errorMessage,
								 libpq_gettext("out of memory\n"));
			return NULL;
		}
	}
	else
	{
		fc_entry = fc_conn->cmd_queue_recycle;
		fc_conn->cmd_queue_recycle = fc_entry->next;
	}
	fc_entry->next = NULL;
	fc_entry->query = NULL;

	return fc_entry;
}

/*
 * pqAppendCmdQueueEntry
 *		将调用者分配的条目附加到命令队列，并更新
 *		conn->asyncStatus 以反映这一点。
 *
 * 查询本身必须已经由调用者放入输出缓冲区。
 */
static void fc_pqAppendCmdQueueEntry(PGconn *fc_conn, PGcmdQueueEntry *fc_entry)
{
	Assert(fc_entry->next == NULL);

	if (fc_conn->cmd_queue_head == NULL)
		fc_conn->cmd_queue_head = fc_entry;
	else
		fc_conn->cmd_queue_tail->next = fc_entry;

	fc_conn->cmd_queue_tail = fc_entry;

	switch (fc_conn->pipelineStatus)
	{
		case PQ_PIPELINE_OFF:
		case PQ_PIPELINE_ON:

			/*
			 * 当不处于管道中断状态时，如果有可以消费的结果，
			 * 就让它被消费（即，不要改变为 READY 或 READY_MORE）；否则设置为忙状态，等待
			 * 从服务器返回的内容。
			 */
			if (fc_conn->asyncStatus == PGASYNC_IDLE)
				fc_conn->asyncStatus = PGASYNC_BUSY;
			break;

		case PQ_PIPELINE_ABORTED:

			/*
			 * 在中断的管道状态下，我们不期待来自
			 * 服务器的任何内容（因为我们不发送任何排队的查询）。
			 * 因此，如果是空闲状态，那么就做 PQgetResult 将会做的事情，以允许
			 * 自己从队列中消费命令；如果我们处于其他任何
			 * 状态，则无需做任何事情。
			 */
			if (fc_conn->asyncStatus == PGASYNC_IDLE ||
				fc_conn->asyncStatus == PGASYNC_PIPELINE_IDLE)
				fc_pqPipelineProcessQueue(fc_conn);
			break;
	}
}

/*
 * pqRecycleCmdQueueEntry
 *		将命令队列条目推入空闲列表。
 */
static void fc_pqRecycleCmdQueueEntry(PGconn *fc_conn, PGcmdQueueEntry *fc_entry)
{
	if (fc_entry == NULL)
		return;

	/* 可回收条目不应有后续命令 */
	Assert(fc_entry->next == NULL);

	if (fc_entry->query)
	{
		free(fc_entry->query);
		fc_entry->query = NULL;
	}

	fc_entry->next = fc_conn->cmd_queue_recycle;
	fc_conn->cmd_queue_recycle = fc_entry;
}


/*
 * PQsendQuery
 *	 提交一个查询，但不等待其完成
 *
 * 返回：1表示成功提交
 *			0表示错误（conn->errorMessage 已设置）
 *
 * PQsendQueryContinue 是一个未导出的版本，行为完全相同，
 * 但不会重置 conn->errorMessage。
 */
int PQsendQuery(PGconn *fc_conn, const char *fc_query)
{
	return fc_PQsendQueryInternal(fc_conn, fc_query, true);
}

int PQsendQueryContinue(PGconn *fc_conn, const char *fc_query)
{
	return fc_PQsendQueryInternal(fc_conn, fc_query, false);
}

static int fc_PQsendQueryInternal(PGconn *fc_conn, const char *fc_query, bool fc_newQuery)
{
	PGcmdQueueEntry *fc_entry = NULL;

	if (!fc_PQsendQueryStart(fc_conn, fc_newQuery))
		return 0;

	/* 检查参数 */
	if (!fc_query)
	{
		appendPQExpBufferStr(&fc_conn->errorMessage,
							 libpq_gettext("command string is a null pointer\n"));
		return 0;
	}

	if (fc_conn->pipelineStatus != PQ_PIPELINE_OFF)
	{
		appendPQExpBuffer(&fc_conn->errorMessage,
						  libpq_gettext("%s not allowed in pipeline mode\n"),
						  "PQsendQuery");
		return 0;
	}

	fc_entry = fc_pqAllocCmdQueueEntry(fc_conn);
	if (fc_entry == NULL)
		return 0;				/* 错误消息已设置 */

	/* 发送查询消息 */
	/* 构造出站查询消息 */
	if (pqPutMsgStart('Q', fc_conn) < 0 ||
		pqPuts(fc_query, fc_conn) < 0 ||
		pqPutMsgEnd(fc_conn) < 0)
	{
		/* 错误消息应该已经设置 */
		fc_pqRecycleCmdQueueEntry(fc_conn, fc_entry);
		return 0;
	}

	/* 记住我们正在使用简单查询协议 */
	fc_entry->queryclass = PGQUERY_SIMPLE;
	/* 如果可能的话，也记住查询文本 */
	fc_entry->query = strdup(fc_query);

	/*
	 * 推送数据。在非阻塞模式下，如果我们无法
	 * 发送所有内容，请不要抱怨；PQgetResult() 将执行任何额外的刷新。
	 */
	if (pqFlush(fc_conn) < 0)
		goto sendFailed;

	/* 好的，已启动！ */
	fc_pqAppendCmdQueueEntry(fc_conn, fc_entry);

	return 1;

sendFailed:
	fc_pqRecycleCmdQueueEntry(fc_conn, fc_entry);
	/* 错误消息应该已经设置 */
	return 0;
}

/*
 * PQsendQueryParams
 *		像 PQsendQuery，但使用扩展查询协议以便于传递参数
 */
int PQsendQueryParams(PGconn *fc_conn,
				  const char *fc_command,
				  int fc_nParams,
				  const Oid *fc_paramTypes,
				  const char *const *fc_paramValues,
				  const int *fc_paramLengths,
				  const int *fc_paramFormats,
				  int fc_resultFormat)
{
	if (!fc_PQsendQueryStart(fc_conn, true))
		return 0;

	/* 检查参数 */
	if (!fc_command)
	{
		appendPQExpBufferStr(&fc_conn->errorMessage,
							 libpq_gettext("command string is a null pointer\n"));
		return 0;
	}
	if (fc_nParams < 0 || fc_nParams > PQ_QUERY_PARAM_MAX_LIMIT)
	{
		appendPQExpBuffer(&fc_conn->errorMessage,
						  libpq_gettext("number of parameters must be between 0 and %d\n"),
						  PQ_QUERY_PARAM_MAX_LIMIT);
		return 0;
	}

	return fc_PQsendQueryGuts(fc_conn,
						   fc_command,
						   "",	/* 使用未命名的语句 */
						   fc_nParams,
						   fc_paramTypes,
						   fc_paramValues,
						   fc_paramLengths,
						   fc_paramFormats,
						   fc_resultFormat);
}

/*
 * PQsendPrepare
 *	 提交一个解析消息，但不等待其完成
 *
 * 返回：1表示成功提交
 *			0表示错误（conn->errorMessage 已设置）
 */
int PQsendPrepare(PGconn *fc_conn,
			  const char *fc_stmtName, const char *fc_query,
			  int fc_nParams, const Oid *fc_paramTypes)
{
	PGcmdQueueEntry *fc_entry = NULL;

	if (!fc_PQsendQueryStart(fc_conn, true))
		return 0;

	/* 检查参数 */
	if (!fc_stmtName)
	{
		appendPQExpBufferStr(&fc_conn->errorMessage,
							 libpq_gettext("statement name is a null pointer\n"));
		return 0;
	}
	if (!fc_query)
	{
		appendPQExpBufferStr(&fc_conn->errorMessage,
							 libpq_gettext("command string is a null pointer\n"));
		return 0;
	}
	if (fc_nParams < 0 || fc_nParams > PQ_QUERY_PARAM_MAX_LIMIT)
	{
		appendPQExpBuffer(&fc_conn->errorMessage,
						  libpq_gettext("number of parameters must be between 0 and %d\n"),
						  PQ_QUERY_PARAM_MAX_LIMIT);
		return 0;
	}

	fc_entry = fc_pqAllocCmdQueueEntry(fc_conn);
	if (fc_entry == NULL)
		return 0;				/* 错误消息已设置 */

	/* 构造解析消息 */
	if (pqPutMsgStart('P', fc_conn) < 0 ||
		pqPuts(fc_stmtName, fc_conn) < 0 ||
		pqPuts(fc_query, fc_conn) < 0)
		goto sendFailed;

	if (fc_nParams > 0 && fc_paramTypes)
	{
		int			fc_i;

		if (pqPutInt(fc_nParams, 2, fc_conn) < 0)
			goto sendFailed;
		for (fc_i = 0; fc_i < fc_nParams; fc_i++)
		{
			if (pqPutInt(fc_paramTypes[fc_i], 4, fc_conn) < 0)
				goto sendFailed;
		}
	}
	else
	{
		if (pqPutInt(0, 2, fc_conn) < 0)
			goto sendFailed;
	}
	if (pqPutMsgEnd(fc_conn) < 0)
		goto sendFailed;

	/* 添加一个同步，除非处于管道模式。 */
	if (fc_conn->pipelineStatus == PQ_PIPELINE_OFF)
	{
		if (pqPutMsgStart('S', fc_conn) < 0 ||
			pqPutMsgEnd(fc_conn) < 0)
			goto sendFailed;
	}

	/* 记住我们仅执行解析 */
	fc_entry->queryclass = PGQUERY_PREPARE;

	/* 如果可能的话，也记住查询文本 */
	/* 如果内存不足，查询最终将为 NULL */
	fc_entry->query = strdup(fc_query);

	/*
	 * 给数据推送 (在管道模式下，仅当我们超过大小阈值时)。在非阻塞模式下，如果我们无法发送所有数据，不要抱怨；PQgetResult()会完成任何额外的刷新。
	 */
	if (fc_pqPipelineFlush(fc_conn) < 0)
		goto sendFailed;

	/* 好的，已启动！ */
	fc_pqAppendCmdQueueEntry(fc_conn, fc_entry);

	return 1;

sendFailed:
	fc_pqRecycleCmdQueueEntry(fc_conn, fc_entry);
	/* 错误消息应该已经设置 */
	return 0;
}

/*
 * PQsendQueryPrepared
 *		类似PQsendQuery，但执行先前准备的语句，
 *		使用扩展查询协议，以便我们可以传递参数
 */
int PQsendQueryPrepared(PGconn *fc_conn,
					const char *fc_stmtName,
					int fc_nParams,
					const char *const *fc_paramValues,
					const int *fc_paramLengths,
					const int *fc_paramFormats,
					int fc_resultFormat)
{
	if (!fc_PQsendQueryStart(fc_conn, true))
		return 0;

	/* 检查参数 */
	if (!fc_stmtName)
	{
		appendPQExpBufferStr(&fc_conn->errorMessage,
							 libpq_gettext("statement name is a null pointer\n"));
		return 0;
	}
	if (fc_nParams < 0 || fc_nParams > PQ_QUERY_PARAM_MAX_LIMIT)
	{
		appendPQExpBuffer(&fc_conn->errorMessage,
						  libpq_gettext("number of parameters must be between 0 and %d\n"),
						  PQ_QUERY_PARAM_MAX_LIMIT);
		return 0;
	}

	return fc_PQsendQueryGuts(fc_conn,
						   NULL,	/* 没有命令可以解析 */
						   fc_stmtName,
						   fc_nParams,
						   NULL,	/* 没有参数类型 */
						   fc_paramValues,
						   fc_paramLengths,
						   fc_paramFormats,
						   fc_resultFormat);
}

/*
 * PQsendQueryStart
 *	PQsendQuery和相关例程的通用启动代码
 */
static bool fc_PQsendQueryStart(PGconn *fc_conn, bool fc_newQuery)
{
	if (!fc_conn)
		return false;

	/*
	 * 如果这是查询周期的开始，重置错误状态。
	 * 然而，在管道模式下，如果有东西已经排队，错误
	 * 缓冲区属于该命令，我们不应该清除它。
	 */
	if (fc_newQuery && fc_conn->cmd_queue_head == NULL)
		pqClearConnErrorState(fc_conn);

	/* 如果我们知道没有活动连接，则不要尝试发送。 */
	if (fc_conn->status != CONNECTION_OK)
	{
		appendPQExpBufferStr(&fc_conn->errorMessage,
							 libpq_gettext("no connection to the server\n"));
		return false;
	}

	/* 如果已经忙着发送，也不能发送，除非排队以便稍后发送 */
	if (fc_conn->asyncStatus != PGASYNC_IDLE &&
		fc_conn->pipelineStatus == PQ_PIPELINE_OFF)
	{
		appendPQExpBufferStr(&fc_conn->errorMessage,
							 libpq_gettext("another command is already in progress\n"));
		return false;
	}

	if (fc_conn->pipelineStatus != PQ_PIPELINE_OFF)
	{
		/*
		 * 在排队命令时，我们不会改变连接的状态，
		 * 因为它已经在当前命令中使用。连接状态将在
		 * pqPipelineProcessQueue()推进以开始处理已排队消息时更新。
		 *
		 * 只需确保根据当前连接状态我们可以安全地排队。
		 * 我们可以在另一个队列项后面排队，或在
		 * 非队列命令（发送其自身同步的命令）后面排队，但我们不能在连接处于复制状态时排队。
		 */
		switch (fc_conn->asyncStatus)
		{
			case PGASYNC_IDLE:
			case PGASYNC_PIPELINE_IDLE:
			case PGASYNC_READY:
			case PGASYNC_READY_MORE:
			case PGASYNC_BUSY:
				/* 可以排队 */
				break;

			case PGASYNC_COPY_IN:
			case PGASYNC_COPY_OUT:
			case PGASYNC_COPY_BOTH:
				appendPQExpBufferStr(&fc_conn->errorMessage,
									 libpq_gettext("cannot queue commands during COPY\n"));
				return false;
		}
	}
	else
	{
		/*
		 * 此命令的结果将立即到达。初始化异步
		 * 结果累积状态
		 */
		pqClearAsyncResult(fc_conn);

		/* 重置单行处理模式 */
		fc_conn->singleRowMode = false;
	}

	/* 准备发送命令消息 */
	return true;
}

/*
 * PQsendQueryGuts
 *		使用扩展查询协议发送查询的通用代码
 *		PQsendQueryStart应该已经完成
 *
 * command可以为NULL以指示我们使用已准备好的语句
 */
static int fc_PQsendQueryGuts(PGconn *fc_conn,
				const char *fc_command,
				const char *fc_stmtName,
				int fc_nParams,
				const Oid *fc_paramTypes,
				const char *const *fc_paramValues,
				const int *fc_paramLengths,
				const int *fc_paramFormats,
				int fc_resultFormat)
{
	int			fc_i;
	PGcmdQueueEntry *fc_entry;

	fc_entry = fc_pqAllocCmdQueueEntry(fc_conn);
	if (fc_entry == NULL)
		return 0;				/* 错误消息已设置 */

	/*
	 * 我们将发送Parse（如果需要）、Bind、Describe Portal、Execute、Sync
	 * （如果不在管道模式下），使用指定的语句名称和
	 * 未命名的portal。
	 */

	if (fc_command)
	{
		/* 构造解析消息 */
		if (pqPutMsgStart('P', fc_conn) < 0 ||
			pqPuts(fc_stmtName, fc_conn) < 0 ||
			pqPuts(fc_command, fc_conn) < 0)
			goto sendFailed;
		if (fc_nParams > 0 && fc_paramTypes)
		{
			if (pqPutInt(fc_nParams, 2, fc_conn) < 0)
				goto sendFailed;
			for (fc_i = 0; fc_i < fc_nParams; fc_i++)
			{
				if (pqPutInt(fc_paramTypes[fc_i], 4, fc_conn) < 0)
					goto sendFailed;
			}
		}
		else
		{
			if (pqPutInt(0, 2, fc_conn) < 0)
				goto sendFailed;
		}
		if (pqPutMsgEnd(fc_conn) < 0)
			goto sendFailed;
	}

	/* 构造Bind消息 */
	if (pqPutMsgStart('B', fc_conn) < 0 ||
		pqPuts("", fc_conn) < 0 ||
		pqPuts(fc_stmtName, fc_conn) < 0)
		goto sendFailed;

	/* 发送参数格式 */
	if (fc_nParams > 0 && fc_paramFormats)
	{
		if (pqPutInt(fc_nParams, 2, fc_conn) < 0)
			goto sendFailed;
		for (fc_i = 0; fc_i < fc_nParams; fc_i++)
		{
			if (pqPutInt(fc_paramFormats[fc_i], 2, fc_conn) < 0)
				goto sendFailed;
		}
	}
	else
	{
		if (pqPutInt(0, 2, fc_conn) < 0)
			goto sendFailed;
	}

	if (pqPutInt(fc_nParams, 2, fc_conn) < 0)
		goto sendFailed;

	/* 发送参数 */
	for (fc_i = 0; fc_i < fc_nParams; fc_i++)
	{
		if (fc_paramValues && fc_paramValues[fc_i])
		{
			int			fc_nbytes;

			if (fc_paramFormats && fc_paramFormats[fc_i] != 0)
			{
				/* 二进制参数 */
				if (fc_paramLengths)
					fc_nbytes = fc_paramLengths[fc_i];
				else
				{
					appendPQExpBufferStr(&fc_conn->errorMessage,
										 libpq_gettext("length must be given for binary parameter\n"));
					goto sendFailed;
				}
			}
			else
			{
				/* 文本参数，不使用paramLengths */
				fc_nbytes = strlen(fc_paramValues[fc_i]);
			}
			if (pqPutInt(fc_nbytes, 4, fc_conn) < 0 ||
				pqPutnchar(fc_paramValues[fc_i], fc_nbytes, fc_conn) < 0)
				goto sendFailed;
		}
		else
		{
			/* 将参数视为NULL */
			if (pqPutInt(-1, 4, fc_conn) < 0)
				goto sendFailed;
		}
	}
	if (pqPutInt(1, 2, fc_conn) < 0 ||
		pqPutInt(fc_resultFormat, 2, fc_conn))
		goto sendFailed;
	if (pqPutMsgEnd(fc_conn) < 0)
		goto sendFailed;

	/* 构造Describe Portal消息 */
	if (pqPutMsgStart('D', fc_conn) < 0 ||
		pqPutc('P', fc_conn) < 0 ||
		pqPuts("", fc_conn) < 0 ||
		pqPutMsgEnd(fc_conn) < 0)
		goto sendFailed;

	/* 构造Execute消息 */
	if (pqPutMsgStart('E', fc_conn) < 0 ||
		pqPuts("", fc_conn) < 0 ||
		pqPutInt(0, 4, fc_conn) < 0 ||
		pqPutMsgEnd(fc_conn) < 0)
		goto sendFailed;

	/* 如果不在管道模式下，构造Sync消息 */
	if (fc_conn->pipelineStatus == PQ_PIPELINE_OFF)
	{
		if (pqPutMsgStart('S', fc_conn) < 0 ||
			pqPutMsgEnd(fc_conn) < 0)
			goto sendFailed;
	}

	/* 记住我们正在使用扩展查询协议 */
	fc_entry->queryclass = PGQUERY_EXTENDED;

	/* 如果可能的话，也记住查询文本 */
	/* 如果内存不足，查询最终将为 NULL */
	if (fc_command)
		fc_entry->query = strdup(fc_command);

	/*
	 * 给数据推送 (在管道模式下，仅当我们超过大小阈值时)。在非阻塞模式下，如果我们无法发送所有数据，不要抱怨；PQgetResult()会完成任何额外的刷新。
	 */
	if (fc_pqPipelineFlush(fc_conn) < 0)
		goto sendFailed;

	/* 好的，已启动！ */
	fc_pqAppendCmdQueueEntry(fc_conn, fc_entry);

	return 1;

sendFailed:
	fc_pqRecycleCmdQueueEntry(fc_conn, fc_entry);
	/* 错误消息应该已经设置 */
	return 0;
}

/*
 * 选择逐行处理模式
 */
int PQsetSingleRowMode(PGconn *fc_conn)
{
	/*
	 * 仅允许在我们已经启动查询且尚未收到任何结果时设置标志。
	 */
	if (!fc_conn)
		return 0;
	if (fc_conn->asyncStatus != PGASYNC_BUSY)
		return 0;
	if (!fc_conn->cmd_queue_head ||
		(fc_conn->cmd_queue_head->queryclass != PGQUERY_SIMPLE &&
		 fc_conn->cmd_queue_head->queryclass != PGQUERY_EXTENDED))
		return 0;
	if (pgHavePendingResult(fc_conn))
		return 0;

	/* 好的，设置标志 */
	fc_conn->singleRowMode = true;
	return 1;
}

/*
 * 消耗后端提供的任何可用输入
 * 0 返回：某种问题
 * 1 返回：没有问题
 */
int PQconsumeInput(PGconn *fc_conn)
{
	if (!fc_conn)
		return 0;

	/*
	 * 对于非阻塞连接，尝试刷新发送队列，否则我们可能永远不会收到
	 * 对于可能尚未发送的内容的响应，因为它在我们的写缓冲区中！
	 */
	if (pqIsnonblocking(fc_conn))
	{
		if (pqFlush(fc_conn) < 0)
			return 0;
	}

	/*
	 * 加载更多数据（如果可用）。无论我们在什么状态下，我们都会这样做，
	 * 因为我们可能是因为应用程序希望摆脱读选择条件才被调用的。请注意，
	 * 我们将不会阻塞等待更多输入。
	 */
	if (pqReadData(fc_conn) < 0)
		return 0;

	/* 数据解析将稍后进行。 */
	return 1;
}


/*
 * parseInput：如适用，从后端解析输入数据
 * 直到输入耗尽或达到停止状态。
 * 请注意，此函数不会尝试从后端读取更多数据。
 */
static void fc_parseInput(PGconn *fc_conn)
{
	pqParseInput3(fc_conn);
}

/*
 * PQisBusy
 *	 如果 PQgetResult 将阻塞等待输入，则返回 true。
 */

int PQisBusy(PGconn *fc_conn)
{
	if (!fc_conn)
		return false;

	/* 如果我们的状态允许，解析任何可用数据。 */
	fc_parseInput(fc_conn);

	/*
	 * PQgetResult 在所有状态下都会立即返回，除了 BUSY。
	 * 此外，如果我们检测到读取 EOF 并断开连接，我们预计
	 * PQgetResult 将立即失败。请注意，我们在这里*不*检查
	 * conn->write_failed --- 一旦设置，就知道我们有问题，
	 * 但我们需要不断尝试读取，直到获得完整的
	 * 服务器消息或检测到读取 EOF。
	 */
	return fc_conn->asyncStatus == PGASYNC_BUSY && fc_conn->status != CONNECTION_BAD;
}

/*
 * PQgetResult
 *	  获取查询生成的下一个 PGresult。如果没有
 *	  查询工作剩余或发生错误（例如，内存不足），返回 NULL。
 *
 *	  在管道模式中，一旦返回了查询的所有结果，
 *	  PQgetResult 将返回 NULL 以让用户知道下一个
 *	  查询正在处理。管道末端，返回
 *	  PQresultStatus(result) == PGRES_PIPELINE_SYNC 的结果。
 */
PGresult * PQgetResult(PGconn *fc_conn)
{
	PGresult   *fc_res;

	if (!fc_conn)
		return NULL;

	/* 如果我们的状态允许，解析任何可用数据。 */
	fc_parseInput(fc_conn);

	/* 如果尚未准备好返回某些内容，则阻塞直到准备好。 */
	while (fc_conn->asyncStatus == PGASYNC_BUSY)
	{
		int			fc_flushResult;

		/*
		 * 如果还有未发送的数据，则发送它。否则我们可能在等待
		 * 后端尚未收到的命令结果。
		 */
		while ((fc_flushResult = pqFlush(fc_conn)) > 0)
		{
			if (pqWait(false, true, fc_conn))
			{
				fc_flushResult = -1;
				break;
			}
		}

		/*
		 * 等待更多数据并加载它。（注意：如果连接已丢失，
		 * pqWait 应立即返回，因为套接字
		 * 应该已准备好读取，要么是最后的服务器数据，要么是 EOF 指示。
		 * 因此，我们期待这不会导致报告先前写入失败的任何不当延迟。）
		 */
		if (fc_flushResult ||
			pqWait(true, false, fc_conn) ||
			pqReadData(fc_conn) < 0)
		{
			/* 报告 pqWait 或 pqReadData 保存的错误 */
			pqSaveErrorResult(fc_conn);
			fc_conn->asyncStatus = PGASYNC_IDLE;
			return pqPrepareAsyncResult(fc_conn);
		}

		/* 解析它。 */
		fc_parseInput(fc_conn);

		/*
		 * 如果我们有写入错误，但上述没有获得查询结果
		 * 或检测到读取错误，则报告写入错误。
		 */
		if (fc_conn->write_failed && fc_conn->asyncStatus == PGASYNC_BUSY)
		{
			fc_pqSaveWriteError(fc_conn);
			fc_conn->asyncStatus = PGASYNC_IDLE;
			return pqPrepareAsyncResult(fc_conn);
		}
	}

	/* 返回适当的内容。 */
	switch (fc_conn->asyncStatus)
	{
		case PGASYNC_IDLE:
			fc_res = NULL;			/* 查询已完成 */
			break;
		case PGASYNC_PIPELINE_IDLE:
			Assert(fc_conn->pipelineStatus != PQ_PIPELINE_OFF);

			/*
			 * 我们即将返回终止当前查询结果的 NULL；准备在下次调用时发送下个查询的结果（如果有的话）。如果命令队列中没有下一个元素，这将使我们进入空闲状态。
			 */
			fc_pqPipelineProcessQueue(fc_conn);
			fc_res = NULL;			/* 查询已完成 */
			break;

		case PGASYNC_READY:
			fc_res = pqPrepareAsyncResult(fc_conn);

			/* 根据需要推进队列 */
			pqCommandQueueAdvance(fc_conn, false,
								  fc_res->resultStatus == PGRES_PIPELINE_SYNC);

			if (fc_conn->pipelineStatus != PQ_PIPELINE_OFF)
			{
				/*
				 * 我们即将发送当前查询的结果。现在将我们设置为空闲状态，并且...
				 */
				fc_conn->asyncStatus = PGASYNC_PIPELINE_IDLE;

				/*
				 * ... 在发送管道同步结果的情况下，立即推进队列处理，以便下次调用时，我们准备返回从服务器接收到的下一个结果。在所有其他情况下，将队列状态的变化留到下次，这样会发送一个终止的 NULL 结果。
				 *
				 * （换句话说：我们不会在管道同步之后返回 NULL。）
				 */
				if (fc_res->resultStatus == PGRES_PIPELINE_SYNC)
					fc_pqPipelineProcessQueue(fc_conn);
			}
			else
			{
				/* 将状态重置为忙碌，允许解析继续进行。 */
				fc_conn->asyncStatus = PGASYNC_BUSY;
			}
			break;
		case PGASYNC_READY_MORE:
			fc_res = pqPrepareAsyncResult(fc_conn);
			/* 将状态重置为忙碌，允许解析继续进行。 */
			fc_conn->asyncStatus = PGASYNC_BUSY;
			break;
		case PGASYNC_COPY_IN:
			fc_res = fc_getCopyResult(fc_conn, PGRES_COPY_IN);
			break;
		case PGASYNC_COPY_OUT:
			fc_res = fc_getCopyResult(fc_conn, PGRES_COPY_OUT);
			break;
		case PGASYNC_COPY_BOTH:
			fc_res = fc_getCopyResult(fc_conn, PGRES_COPY_BOTH);
			break;
		default:
			appendPQExpBuffer(&fc_conn->errorMessage,
							  libpq_gettext("unexpected asyncStatus: %d\n"),
							  (int) fc_conn->asyncStatus);
			pqSaveErrorResult(fc_conn);
			fc_conn->asyncStatus = PGASYNC_IDLE;	/* 尝试恢复有效状态 */
			fc_res = pqPrepareAsyncResult(fc_conn);
			break;
	}

	/* 是时候触发 PGEVT_RESULTCREATE 事件，如果有的话 */
	if (fc_res && fc_res->nEvents > 0)
		(void) PQfireResultCreateEvents(fc_conn, fc_res);

	return fc_res;
}

/*
 * getCopyResult
 *	  PQgetResult 的辅助函数：为正在进行的 COPY 生成结果
 */
static PGresult * fc_getCopyResult(PGconn *fc_conn, ExecStatusType fc_copytype)
{
	/*
	 * 如果服务器连接已丢失，不要假装一切都很好；而是返回一个 PGRES_FATAL_ERROR 结果，并将 asyncStatus 重置为空闲状态（对应于我们在 PQgetResult 的早期步骤中检测到 I/O 错误时的操作）。返回结果中的文本为 conn->errorMessage 中的内容；我们希望当检测到连接丢失时，这里能填入一些相关的信息。
	 */
	if (fc_conn->status != CONNECTION_OK)
	{
		pqSaveErrorResult(fc_conn);
		fc_conn->asyncStatus = PGASYNC_IDLE;
		return pqPrepareAsyncResult(fc_conn);
	}

	/* 如果我们有 COPY 的异步结果，返回该结果 */
	if (fc_conn->result && fc_conn->result->resultStatus == fc_copytype)
		return pqPrepareAsyncResult(fc_conn);

	/* 否则，构造一个合适的 PGresult */
	return PQmakeEmptyPGresult(fc_conn, fc_copytype);
}


/*
 * PQexec
 *	  向后端发送查询并将结果打包为 PGresult
 *
 * 如果查询甚至没有发送，返回 NULL；conn->errorMessage 设置为相关消息。
 * 如果查询已发送，返回一个新的 PGresult（可能表示成功或失败）。
 * 用户在使用完该 PGresult 后有责任通过 PQclear() 释放它。
 */
PGresult * PQexec(PGconn *fc_conn, const char *fc_query)
{
	if (!fc_PQexecStart(fc_conn))
		return NULL;
	if (!PQsendQuery(fc_conn, fc_query))
		return NULL;
	return fc_PQexecFinish(fc_conn);
}

/*
 * PQexecParams
 *		像 PQexec，但使用扩展查询协议，以便我们可以传递参数
 */
PGresult * PQexecParams(PGconn *fc_conn,
			 const char *fc_command,
			 int fc_nParams,
			 const Oid *fc_paramTypes,
			 const char *const *fc_paramValues,
			 const int *fc_paramLengths,
			 const int *fc_paramFormats,
			 int fc_resultFormat)
{
	if (!fc_PQexecStart(fc_conn))
		return NULL;
	if (!PQsendQueryParams(fc_conn, fc_command,
						   fc_nParams, fc_paramTypes, fc_paramValues, fc_paramLengths,
						   fc_paramFormats, fc_resultFormat))
		return NULL;
	return fc_PQexecFinish(fc_conn);
}

/*
 * PQprepare
 *	  通过发送解析消息创建一个准备好的语句。
 *
 * 如果查询甚至没有发送，返回 NULL；conn->errorMessage 设置为相关消息。
 * 如果查询已发送，返回一个新的 PGresult（可能表示成功或失败）。
 * 用户在使用完该 PGresult 后有责任通过 PQclear() 释放它。
 */
PGresult * PQprepare(PGconn *fc_conn,
		  const char *fc_stmtName, const char *fc_query,
		  int fc_nParams, const Oid *fc_paramTypes)
{
	if (!fc_PQexecStart(fc_conn))
		return NULL;
	if (!PQsendPrepare(fc_conn, fc_stmtName, fc_query, fc_nParams, fc_paramTypes))
		return NULL;
	return fc_PQexecFinish(fc_conn);
}

/*
 * PQexecPrepared
 *		像 PQexec，但执行先前准备的语句，
 *		使用扩展查询协议，以便我们可以传递参数
 */
PGresult * PQexecPrepared(PGconn *fc_conn,
			   const char *fc_stmtName,
			   int fc_nParams,
			   const char *const *fc_paramValues,
			   const int *fc_paramLengths,
			   const int *fc_paramFormats,
			   int fc_resultFormat)
{
	if (!fc_PQexecStart(fc_conn))
		return NULL;
	if (!PQsendQueryPrepared(fc_conn, fc_stmtName,
							 fc_nParams, fc_paramValues, fc_paramLengths,
							 fc_paramFormats, fc_resultFormat))
		return NULL;
	return fc_PQexecFinish(fc_conn);
}

/*
 * PQexec 和兄弟例程的公共代码：准备发送命令
 */
static bool fc_PQexecStart(PGconn *fc_conn)
{
	PGresult   *fc_result;

	if (!fc_conn)
		return false;

	/*
	 * 由于这是查询周期的开始，重置错误状态。
	 * 然而，在管道模式下，如果有命令已经排队，则错误
	 * 缓冲区属于该命令，我们不应该清除它。
	 */
	if (fc_conn->cmd_queue_head == NULL)
		pqClearConnErrorState(fc_conn);

	if (fc_conn->pipelineStatus != PQ_PIPELINE_OFF)
	{
		appendPQExpBufferStr(&fc_conn->errorMessage,
							 libpq_gettext("synchronous command execution functions are not allowed in pipeline mode\n"));
		return false;
	}

	/*
	 * 安静地丢弃应用程序没有处理的任何先前查询结果。
	 * 这可能是设计不佳，但由于向后兼容性，它就在这里。
	 */
	while ((fc_result = PQgetResult(fc_conn)) != NULL)
	{
		ExecStatusType fc_resultStatus = fc_result->resultStatus;

		PQclear(fc_result);		/* 只需要其状态 */
		if (fc_resultStatus == PGRES_COPY_IN)
		{
			/* 退出 COPY IN 状态 */
			if (PQputCopyEnd(fc_conn,
							 libpq_gettext("COPY terminated by new PQexec")) < 0)
				return false;
			/* 继续等待以吞下复制的失败消息 */
		}
		else if (fc_resultStatus == PGRES_COPY_OUT)
		{
			/*
			 * 退出 COPY OUT 状态：我们只需切换回 BUSY，
			 * 并允许剩余的 COPY 数据掉落在地上。
			 */
			fc_conn->asyncStatus = PGASYNC_BUSY;
			/* 继续等待以吞下复制的完成消息 */
		}
		else if (fc_resultStatus == PGRES_COPY_BOTH)
		{
			/* 在 COPY BOTH 期间不允许 PQexec */
			appendPQExpBufferStr(&fc_conn->errorMessage,
								 libpq_gettext("PQexec not allowed during COPY BOTH\n"));
			return false;
		}
		/* 也检查连接是否丢失 */
		if (fc_conn->status == CONNECTION_BAD)
			return false;
	}

	/* 发送命令是可以的 */
	return true;
}

/*
 * PQexec 和兄弟例程的通用代码：等待命令结果
 */
static PGresult * fc_PQexecFinish(PGconn *fc_conn)
{
	PGresult   *fc_result;
	PGresult   *fc_lastResult;

	/*
	 * 为了向后兼容，如果有多个结果则返回最后一个。
	 * （我们曾经在这里有逻辑来串联连续的错误
	 * 消息，但现在这是自动进行的，因为 conn->errorMessage
	 * 会在整个循环中继续累积错误。）
	 *
	 * 但是，如果我们看到 copy in/out/both，我们必须停止。
	 * 我们将在应用程序执行数据传输后恢复解析。
	 *
	 * 如果连接丢失也要停止（否则我们将无限循环）。
	 */
	fc_lastResult = NULL;
	while ((fc_result = PQgetResult(fc_conn)) != NULL)
	{
		if (fc_lastResult)
			PQclear(fc_lastResult);
		fc_lastResult = fc_result;
		if (fc_result->resultStatus == PGRES_COPY_IN ||
			fc_result->resultStatus == PGRES_COPY_OUT ||
			fc_result->resultStatus == PGRES_COPY_BOTH ||
			fc_conn->status == CONNECTION_BAD)
			break;
	}

	return fc_lastResult;
}

/*
 * PQdescribePrepared
 *	  获取有关先前准备的语句的信息
 *
 * 如果查询甚至没有发送，则返回 NULL；conn->errorMessage 被设置为
 * 相关消息。
 * 如果查询已发送，返回一个新的 PGresult（这可能表示
 * 成功或失败）。在成功时，PGresult 包含状态
 * PGRES_COMMAND_OK，且其参数和列标题字段描述
 * 该语句的输入和输出。
 * 用户在完成后负责通过 PQclear() 释放 PGresult。
 */
PGresult * PQdescribePrepared(PGconn *fc_conn, const char *fc_stmt)
{
	if (!fc_PQexecStart(fc_conn))
		return NULL;
	if (!fc_PQsendDescribe(fc_conn, 'S', fc_stmt))
		return NULL;
	return fc_PQexecFinish(fc_conn);
}

/*
 * PQdescribePortal
 *	  获取有关先前创建的门户的信息
 *
 * 这与 PQdescribePrepared 非常相似，只是没有返回参数信息。
 * 请注意，目前 libpq 并没有真正将门户暴露给客户端；
 * 但这可以与通过 SQL DECLARE CURSOR 命令创建的门户一起使用。
 */
PGresult * PQdescribePortal(PGconn *fc_conn, const char *fc_portal)
{
	if (!fc_PQexecStart(fc_conn))
		return NULL;
	if (!fc_PQsendDescribe(fc_conn, 'P', fc_portal))
		return NULL;
	return fc_PQexecFinish(fc_conn);
}

/*
 * PQsendDescribePrepared
 *	 提交一个描述语句命令，但不等待其完成
 *
 * 返回：成功提交返回 1
 *			错误返回 0（设置了 conn->errorMessage）
 */
int PQsendDescribePrepared(PGconn *fc_conn, const char *fc_stmt)
{
	return fc_PQsendDescribe(fc_conn, 'S', fc_stmt);
}

/*
 * PQsendDescribePortal
 *	 提交一个描述门户命令，但不等待其完成
 *
 * 返回：成功提交返回 1
 *			错误返回 0（设置了 conn->errorMessage）
 */
int PQsendDescribePortal(PGconn *fc_conn, const char *fc_portal)
{
	return fc_PQsendDescribe(fc_conn, 'P', fc_portal);
}

/*
 * PQsendDescribe
 *	 发送描述命令的通用代码
 *
 * desc_type 的可用选项是
 *	 'S' 描述一个准备好的语句；或
 *	 'P' 描述一个门户。
 * 成功返回 1，失败返回 0。
 */
static int fc_PQsendDescribe(PGconn *fc_conn, char fc_desc_type, const char *fc_desc_target)
{
	PGcmdQueueEntry *fc_entry = NULL;

	/* 将 null desc_target 视为空字符串 */
	if (!fc_desc_target)
		fc_desc_target = "";

	if (!fc_PQsendQueryStart(fc_conn, true))
		return 0;

	fc_entry = fc_pqAllocCmdQueueEntry(fc_conn);
	if (fc_entry == NULL)
		return 0;				/* 错误消息已设置 */

	/* 构造描述消息 */
	if (pqPutMsgStart('D', fc_conn) < 0 ||
		pqPutc(fc_desc_type, fc_conn) < 0 ||
		pqPuts(fc_desc_target, fc_conn) < 0 ||
		pqPutMsgEnd(fc_conn) < 0)
		goto sendFailed;

	/* 构造同步消息 */
	if (fc_conn->pipelineStatus == PQ_PIPELINE_OFF)
	{
		if (pqPutMsgStart('S', fc_conn) < 0 ||
			pqPutMsgEnd(fc_conn) < 0)
			goto sendFailed;
	}

	/* 记住我们正在进行描述 */
	fc_entry->queryclass = PGQUERY_DESCRIBE;

	/*
	 * 给数据推送 (在管道模式下，仅当我们超过大小阈值时)。在非阻塞模式下，如果我们无法发送所有数据，不要抱怨；PQgetResult()会完成任何额外的刷新。
	 */
	if (fc_pqPipelineFlush(fc_conn) < 0)
		goto sendFailed;

	/* 好的，已启动！ */
	fc_pqAppendCmdQueueEntry(fc_conn, fc_entry);

	return 1;

sendFailed:
	fc_pqRecycleCmdQueueEntry(fc_conn, fc_entry);
	/* 错误消息应该已经设置 */
	return 0;
}

/*
 * PQnotifies
 *	  返回最新的异步通知的 PGnotify* 结构，该通知尚未处理
 *
 * 如果当前没有来自后端的未处理异步通知，返回 NULL
 *
 * 调用者负责释放返回的结构
 *
 * 请注意，此函数不会从套接字读取任何新数据；通常，调用者应该首先调用 PQconsumeInput()。
 */
PGnotify *
PQnotifies(PGconn *fc_conn)
{
	PGnotify   *fc_event;

	if (!fc_conn)
		return NULL;

	/* 解析任何可用数据，以查看我们是否可以提取 NOTIFY 消息。 */
	fc_parseInput(fc_conn);

	fc_event = fc_conn->notifyHead;
	if (fc_event)
	{
		fc_conn->notifyHead = fc_event->next;
		if (!fc_conn->notifyHead)
			fc_conn->notifyTail = NULL;
		fc_event->next = NULL;		/* 不让应用程序看到内部状态 */
	}
	return fc_event;
}

/*
 * PQputCopyData - 在 COPY IN 或 COPY BOTH 期间将一些数据发送到后端
 *
 * 成功时返回 1，如果数据无法发送则返回 0（仅在非阻塞模式中可能），出错时返回 -1。
 */
int PQputCopyData(PGconn *fc_conn, const char *fc_buffer, int fc_nbytes)
{
	if (!fc_conn)
		return -1;
	if (fc_conn->asyncStatus != PGASYNC_COPY_IN &&
		fc_conn->asyncStatus != PGASYNC_COPY_BOTH)
	{
		appendPQExpBufferStr(&fc_conn->errorMessage,
							 libpq_gettext("no COPY in progress\n"));
		return -1;
	}

	/*
	 * 处理输入缓冲区中可能待处理的任何 NOTICE 或 NOTIFY 消息。由于服务器可能在 COPY 期间生成许多通知，我们希望合理及时地清除这些通知，以防止输入缓冲区无限扩展。（注意：实际将输入数据读取到输入缓冲区发生在 pqSendSome 内部，但它没有权威处理数据的清除。）
	 */
	fc_parseInput(fc_conn);

	if (fc_nbytes > 0)
	{
		/*
		 * 尽量刷新之前发送的数据，而不是增加输出缓冲区。如果我们无法将缓冲区扩大到足以容纳数据，则在非阻塞情况下返回 0，否则返回硬错误。（为简单起见，始终假设有 5 字节的开销。）
		 */
		if ((fc_conn->outBufSize - fc_conn->outCount - 5) < fc_nbytes)
		{
			if (pqFlush(fc_conn) < 0)
				return -1;
			if (pqCheckOutBufferSpace(fc_conn->outCount + 5 + (size_t) fc_nbytes,
									  fc_conn))
				return pqIsnonblocking(fc_conn) ? 0 : -1;
		}
		/* 发送数据（过于简单，以至于不委托给 fe-protocol 文件） */
		if (pqPutMsgStart('d', fc_conn) < 0 ||
			pqPutnchar(fc_buffer, fc_nbytes, fc_conn) < 0 ||
			pqPutMsgEnd(fc_conn) < 0)
			return -1;
	}
	return 1;
}

/*
 * PQputCopyEnd - 在 COPY IN 期间向后端发送 EOF 指示
 *
 * 调用此方法后，使用 PQgetResult() 检查命令完成状态。
 *
 * 成功时返回 1，如果数据无法发送则返回 0（仅在非阻塞模式中可能），出错时返回 -1。
 */
int PQputCopyEnd(PGconn *fc_conn, const char *fc_errormsg)
{
	if (!fc_conn)
		return -1;
	if (fc_conn->asyncStatus != PGASYNC_COPY_IN &&
		fc_conn->asyncStatus != PGASYNC_COPY_BOTH)
	{
		appendPQExpBufferStr(&fc_conn->errorMessage,
							 libpq_gettext("no COPY in progress\n"));
		return -1;
	}

	/*
	 * 发送 COPY END 指示符。这简单到不需要将其委托给 fe-protocol 文件。
	 */
	if (fc_errormsg)
	{
		/* 发送 COPY FAIL */
		if (pqPutMsgStart('f', fc_conn) < 0 ||
			pqPuts(fc_errormsg, fc_conn) < 0 ||
			pqPutMsgEnd(fc_conn) < 0)
			return -1;
	}
	else
	{
		/* 发送 COPY DONE */
		if (pqPutMsgStart('c', fc_conn) < 0 ||
			pqPutMsgEnd(fc_conn) < 0)
			return -1;
	}

	/*
	 * 如果我们在扩展查询模式下发送了 COPY 命令，我们也必须发出一个 Sync。
	 */
	if (fc_conn->cmd_queue_head &&
		fc_conn->cmd_queue_head->queryclass != PGQUERY_SIMPLE)
	{
		if (pqPutMsgStart('S', fc_conn) < 0 ||
			pqPutMsgEnd(fc_conn) < 0)
			return -1;
	}

	/* 返回到主动状态 */
	if (fc_conn->asyncStatus == PGASYNC_COPY_BOTH)
		fc_conn->asyncStatus = PGASYNC_COPY_OUT;
	else
		fc_conn->asyncStatus = PGASYNC_BUSY;

	/* 尝试刷新数据 */
	if (pqFlush(fc_conn) < 0)
		return -1;

	return 1;
}

/*
 * PQgetCopyData - 从后端读取COPY OUT或COPY BOTH过程中的一行数据
 *
 * 如果成功，设置 *buffer 指向一个 malloc 分配的行数据，并
 * 返回行长度（总是 > 0）作为结果。
 * 如果尚无可用行，则返回 0（仅在 async 为 true 时可能），
 * 如果复制结束则返回 -1（请参阅 PQgetResult），或如果错误则返回 -2（请参阅
 * PQerrorMessage）。
 */
int PQgetCopyData(PGconn *fc_conn, char **fc_buffer, int fc_async)
{
	*fc_buffer = NULL;				/* 所有失败情况 */
	if (!fc_conn)
		return -2;
	if (fc_conn->asyncStatus != PGASYNC_COPY_OUT &&
		fc_conn->asyncStatus != PGASYNC_COPY_BOTH)
	{
		appendPQExpBufferStr(&fc_conn->errorMessage,
							 libpq_gettext("no COPY in progress\n"));
		return -2;
	}
	return pqGetCopyData3(fc_conn, fc_buffer, fc_async);
}

/*
 * PQgetline - 从后端获取以换行符结尾的字符串。
 *
 * 主要是为了让应用程序能够使用 "COPY <rel> to stdout"
 * 并读取输出字符串。返回一个以空字符结尾的字符串 s。
 *
 * XXX 该例程现在不推荐使用，因为它无法处理二进制数据。
 * 如果在 COPY BINARY 期间调用则返回 EOF。
 *
 * PQgetline 最多读取 maxlen-1 个字符（如 fgets(3)），但去掉
 * 终止的 \n（如 gets(3)）。
 *
 * 注意：调用者负责检测结束复制信号（
 * 仅包含 "\." 的行）时使用此例程。
 *
 * 返回：
 *		如果错误则返回 EOF（例如，提供了无效参数）
 *		如果达到行尾则返回 0（即，已读取到 \n）
 *				（这是向后兼容的要求 -- 该
 *				 例程以前总是返回 EOF 或 0，假设
 *				 行在 maxlen 字节内结束。）
 *		在其他情况下返回 1（即，缓冲区在 \n 到达之前已填满）
 */
int PQgetline(PGconn *fc_conn, char *fc_s, int fc_maxlen)
{
	if (!fc_s || fc_maxlen <= 0)
		return EOF;
	*fc_s = '\0';
	/* maxlen 必须至少为 3，以容纳 \. 终止符！ */
	if (fc_maxlen < 3)
		return EOF;

	if (!fc_conn)
		return EOF;

	return pqGetline3(fc_conn, fc_s, fc_maxlen);
}

/*
 * PQgetlineAsync - 无阻塞获取COPY数据行。
 *
 * 此例程适用于希望异步执行 "COPY <rel> to stdout"
 * 的应用程序，即不阻塞。发出 COPY 命令并获得 PGRES_COPY_OUT 响应后，
 * 应用程序应调用 PQconsumeInput 和此例程，直到检测到结束数据信号。与
 * PQgetline 不同，此例程负责检测结束数据。
 *
 * 每次调用时，如果 libpq 输入缓冲区中有完整数据行可用，PQgetlineAsync 将返回数据。
 * 否则，直到其余行到达之前，不返回数据。
 *
 * 如果返回 -1，则表示已识别结束数据信号（并已从 libpq 输入缓冲区中移除）。
 * 调用者 *必须* 接下来调用 PQendcopy，然后返回正常处理。
 *
 * 返回：
 *	 -1    如果已识别结束复制数据标记
 *	 0	   如果没有数据可用
 *	 >0    返回的字节数。
 *
 * 返回的数据不会超出数据行边界。如果可能，
 * 将一次返回整行。但如果调用者提供的缓冲区太小，
 * 以至于无法容纳后端发送的行，则将返回部分
 * 数据行。在文本模式下，这可以通过测试
 * 最后返回的字节是否为 '\n' 来检测。
 *
 * 返回的数据 *不是* 以空字符结尾。
 */

int PQgetlineAsync(PGconn *fc_conn, char *fc_buffer, int fc_bufsize)
{
	if (!fc_conn)
		return -1;

	return pqGetlineAsync3(fc_conn, fc_buffer, fc_bufsize);
}

/*
 * PQputline -- 在COPY IN过程中将字符串发送到后端。
 * 如果成功返回0，如果失败返回EOF。
 *
 * 这个函数已经被弃用，主要是因为返回约定不允许
 * 调用者区分严重错误和非阻塞模式下的发送失败。
 */
int PQputline(PGconn *fc_conn, const char *fc_s)
{
	return PQputnbytes(fc_conn, fc_s, strlen(fc_s));
}

/*
 * PQputnbytes -- 类似于PQputline，但缓冲区不需要以null结尾。
 * 如果成功返回0，如果失败返回EOF。
 */
int PQputnbytes(PGconn *fc_conn, const char *fc_buffer, int fc_nbytes)
{
	if (PQputCopyData(fc_conn, fc_buffer, fc_nbytes) > 0)
		return 0;
	else
		return EOF;
}

/*
 * PQendcopy
 *		在完成COPY进/出的数据传输部分后，
 *		应用程序必须调用此例程以完成命令协议。
 *
 * 这个函数已经被弃用；使用PQgetResult获取传输
 * 状态更为清晰。
 *
 * 返回：
 *		成功返回0
 *		失败返回1
 */
int PQendcopy(PGconn *fc_conn)
{
	if (!fc_conn)
		return 0;

	return pqEndcopy3(fc_conn);
}


/* ----------------
 *		PQfn - 向POSTGRES后端发送函数调用。
 *
 *		conn			：后端连接
 *		fnid			：要调用的函数的OID
 *		result_buf		：指向结果缓冲区的指针
 *		result_len		：实际结果长度在此返回
 *		result_is_int	：如果结果为整数，则必须为1，
 *						  否则应为0
 *		args			：指向函数参数数组的指针
 *						  （每个有长度，如果是整数，则有值/指针）
 *		nargs			：args数组中的参数数量。
 *
 * 返回
 *		如果成功，则返回状态为PGRES_COMMAND_OK的PGresult。
 *			*result_len如果有返回值则大于0，若没有则为0。
 *		如果后端返回错误，则返回状态为PGRES_FATAL_ERROR的PGresult。
 *		在通信失败时返回NULL。conn->errorMessage将被设置。
 * ----------------
 */

PGresult * PQfn(PGconn *fc_conn,
	 int fc_fnid,
	 int *fc_result_buf,
	 int *fc_result_len,
	 int fc_result_is_int,
	 const PQArgBlock *fc_args,
	 int fc_nargs)
{
	*fc_result_len = 0;

	if (!fc_conn)
		return NULL;

	/*
	 * 由于这是查询周期的开始，重置错误状态。
	 * 然而，在管道模式下，如果有命令已经排队，则错误
	 * 缓冲区属于该命令，我们不应该清除它。
	 */
	if (fc_conn->cmd_queue_head == NULL)
		pqClearConnErrorState(fc_conn);

	if (fc_conn->pipelineStatus != PQ_PIPELINE_OFF)
	{
		appendPQExpBuffer(&fc_conn->errorMessage,
						  libpq_gettext("%s not allowed in pipeline mode\n"),
						  "PQfn");
		return NULL;
	}

	if (fc_conn->sock == PGINVALID_SOCKET || fc_conn->asyncStatus != PGASYNC_IDLE ||
		pgHavePendingResult(fc_conn))
	{
		appendPQExpBufferStr(&fc_conn->errorMessage,
							 libpq_gettext("connection in wrong state\n"));
		return NULL;
	}

	return pqFunctionCall3(fc_conn, fc_fnid,
						   fc_result_buf, fc_result_len,
						   fc_result_is_int,
						   fc_args, fc_nargs);
}

/* ====== 管道模式支持 ======== */

/*
 * PQenterPipelineMode
 *		将空闲连接置于管道模式。
 *
 * 成功时返回1。如果失败，errorMessage被设置并返回0。
 *
 * 此后提交的命令可以在连接上进行管道处理；
 * 不需要等待一个完成后再派发下一个。
 *
 * 不允许在COPY期间排队新查询或同步。
 *
 * 一组命令以PQpipelineSync结束。在管道模式下可以建立多个同步
 * 点。在处理完所有结果后，可以通过调用PQexitPipelineMode()退出
 * 管道模式。
 *
 * 这实际上并没有在网络上传输任何内容，它只是将libpq
 * 置于可以管道处理工作的状态。
 */
int PQenterPipelineMode(PGconn *fc_conn)
{
	if (!fc_conn)
		return 0;

	/* 如果已经在管道模式下则成功且无操作 */
	if (fc_conn->pipelineStatus != PQ_PIPELINE_OFF)
		return 1;

	if (fc_conn->asyncStatus != PGASYNC_IDLE)
	{
		appendPQExpBufferStr(&fc_conn->errorMessage,
							 libpq_gettext("cannot enter pipeline mode, connection not idle\n"));
		return 0;
	}

	fc_conn->pipelineStatus = PQ_PIPELINE_ON;

	return 1;
}

/*
 * PQexitPipelineMode
 *		结束管道模式并返回正常命令模式。
 *
 * 成功时返回1（管道模式成功结束，或未处于管道
 * 模式）。
 *
 * 如果在管道模式中且尚不能结束则返回0。错误信息将被
 * 设置。
 */
int PQexitPipelineMode(PGconn *fc_conn)
{
	if (!fc_conn)
		return 0;

	if (fc_conn->pipelineStatus == PQ_PIPELINE_OFF &&
		(fc_conn->asyncStatus == PGASYNC_IDLE ||
		 fc_conn->asyncStatus == PGASYNC_PIPELINE_IDLE) &&
		fc_conn->cmd_queue_head == NULL)
		return 1;

	switch (fc_conn->asyncStatus)
	{
		case PGASYNC_READY:
		case PGASYNC_READY_MORE:
			/* 仍有一些未收集的结果 */
			appendPQExpBufferStr(&fc_conn->errorMessage,
								 libpq_gettext("cannot exit pipeline mode with uncollected results\n"));
			return 0;

		case PGASYNC_BUSY:
			appendPQExpBufferStr(&fc_conn->errorMessage,
								 libpq_gettext("cannot exit pipeline mode while busy\n"));
			return 0;

		case PGASYNC_IDLE:
		case PGASYNC_PIPELINE_IDLE:
			/* 好的 */
			break;

		case PGASYNC_COPY_IN:
		case PGASYNC_COPY_OUT:
		case PGASYNC_COPY_BOTH:
			appendPQExpBufferStr(&fc_conn->errorMessage,
								 libpq_gettext("cannot exit pipeline mode while in COPY\n"));
	}

	/* 仍有工作要处理 */
	if (fc_conn->cmd_queue_head != NULL)
	{
		appendPQExpBufferStr(&fc_conn->errorMessage,
							 libpq_gettext("cannot exit pipeline mode with uncollected results\n"));
		return 0;
	}

	fc_conn->pipelineStatus = PQ_PIPELINE_OFF;
	fc_conn->asyncStatus = PGASYNC_IDLE;

	/* 刷新输出缓冲区中的任何待处理数据 */
	if (pqFlush(fc_conn) < 0)
		return 0;				/* 错误消息已设置 */
	return 1;
}

/*
 * pqCommandQueueAdvance
 *		从命令队列中移除一个查询，如果合适的话。
 *
 * 如果我们已经收到了对应于命令队列头元素的所有结果，
 * 就移除它。
 *
 * 在简单查询协议中，我们必须等到接收到 ReadyForQuery 消息后
 * 才能推进命令队列。这是因为在简单模式下，一个命令可以有多个查询，
 * 我们必须处理所有查询的结果后才能继续下一个命令。
 *
 * 另一个考虑是在扩展查询协议中的错误处理时的同步：我们拒绝将队列推进
 * 超过 SYNC 队列元素，除非我们接收到的结果也是 SYNC。特别是
 * 这保护我们在不合适的时刻收到错误时不推进。
 */
void pqCommandQueueAdvance(PGconn *fc_conn, bool fc_isReadyForQuery, bool fc_gotSync)
{
	PGcmdQueueEntry *fc_prevquery;

	if (fc_conn->cmd_queue_head == NULL)
		return;

	/*
	 * 如果处理简单查询协议中的一个查询，我们只在收到
	 * 对应的 ReadyForQuery 消息时推进队列。
	 */
	if (fc_conn->cmd_queue_head->queryclass == PGQUERY_SIMPLE && !fc_isReadyForQuery)
		return;

	/*
	 * 如果我们在等待 SYNC，不要推进队列直到我们得到一个。
	 */
	if (fc_conn->cmd_queue_head->queryclass == PGQUERY_SYNC && !fc_gotSync)
		return;

	/* 从队列中解除链接元素 */
	fc_prevquery = fc_conn->cmd_queue_head;
	fc_conn->cmd_queue_head = fc_conn->cmd_queue_head->next;

	/* 如果队列现在为空，也重置尾部 */
	if (fc_conn->cmd_queue_head == NULL)
		fc_conn->cmd_queue_tail = NULL;

	/* 并使队列元素可回收 */
	fc_prevquery->next = NULL;
	fc_pqRecycleCmdQueueEntry(fc_conn, fc_prevquery);
}

/*
 * pqPipelineProcessQueue: PQgetResult 的子例程
 *		在管道模式下，开始处理队列中下一个查询的结果。
 */
static void fc_pqPipelineProcessQueue(PGconn *fc_conn)
{
	switch (fc_conn->asyncStatus)
	{
		case PGASYNC_COPY_IN:
		case PGASYNC_COPY_OUT:
		case PGASYNC_COPY_BOTH:
		case PGASYNC_READY:
		case PGASYNC_READY_MORE:
		case PGASYNC_BUSY:
			/* 客户端仍然需要处理当前查询或结果 */
			return;

		case PGASYNC_IDLE:

			/*
			 * 如果我们处于空闲模式，并且队列中有一些命令，
			 * 进入 PIPELINE_IDLE 模式并正常处理。否则
			 * 我们没有任何事情可做。
			 */
			if (fc_conn->cmd_queue_head != NULL)
			{
				fc_conn->asyncStatus = PGASYNC_PIPELINE_IDLE;
				break;
			}
			return;

		case PGASYNC_PIPELINE_IDLE:
			Assert(fc_conn->pipelineStatus != PQ_PIPELINE_OFF);
			/* 下一个查询请 */
			break;
	}

	/*
	 * 重置单行处理模式。 （客户端需要在每个查询时设置，
	 * 如果需要的话。）
	 */
	fc_conn->singleRowMode = false;

	/*
	 * 如果队列中没有进一步的命令需要处理，立即进入
	 * “真正的空闲”模式。
	 */
	if (fc_conn->cmd_queue_head == NULL)
	{
		fc_conn->asyncStatus = PGASYNC_IDLE;
		return;
	}

	/*
	 * 重置错误状态。这个和接下来的几个步骤对应于
	 * PQsendQueryStart 没有为这个查询完成的操作。
	 */
	pqClearConnErrorState(fc_conn);

	/* 初始化异步结果累积状态 */
	pqClearAsyncResult(fc_conn);

	if (fc_conn->pipelineStatus == PQ_PIPELINE_ABORTED &&
		fc_conn->cmd_queue_head->queryclass != PGQUERY_SYNC)
	{
		/*
		 * 在一个被中止的管道中，我们不会从服务器收到
		 * 每个结果的任何信息；我们只是在丢弃队列中的命令，直到
		 * 到达服务器的下一个同步。
		 *
		 * PGRES_PIPELINE_ABORTED 结果告诉客户端它的查询
		 * 被中止了。
		 */
		fc_conn->result = PQmakeEmptyPGresult(fc_conn, PGRES_PIPELINE_ABORTED);
		if (!fc_conn->result)
		{
			appendPQExpBufferStr(&fc_conn->errorMessage,
								 libpq_gettext("out of memory\n"));
			pqSaveErrorResult(fc_conn);
			return;
		}
		fc_conn->asyncStatus = PGASYNC_READY;
	}
	else
	{
		/* 允许继续解析 */
		fc_conn->asyncStatus = PGASYNC_BUSY;
	}
}

/*
 * PQpipelineSync
 *		作为管道的一部分发送同步消息，并刷新到服务器
 *
 * 允许立即开始在管道中提交更多命令，而不必等待当前管道的结果。没有必要结束管道模式并重新开始。
 *
 * 如果管道中的一个命令失败，则每个后续命令（包括发送的同步消息的结果）都将被设置为PGRES_PIPELINE_ABORTED状态。如果整个管道在没有错误的情况下处理，则生成一个带有PGRES_PIPELINE_SYNC的PGresult。
 *
 * 在调用PQpipelineSync之前，查询可能已经被发送，但必须在检索命令结果之前调用PQpipelineSync。
 *
 * 连接将保持在管道模式，并且在客户端处理完管道中的所有结果之前，无法执行新的同步命令。
 */
int PQpipelineSync(PGconn *fc_conn)
{
	PGcmdQueueEntry *fc_entry;

	if (!fc_conn)
		return 0;

	if (fc_conn->pipelineStatus == PQ_PIPELINE_OFF)
	{
		appendPQExpBufferStr(&fc_conn->errorMessage,
							 libpq_gettext("cannot send pipeline when not in pipeline mode\n"));
		return 0;
	}

	switch (fc_conn->asyncStatus)
	{
		case PGASYNC_COPY_IN:
		case PGASYNC_COPY_OUT:
		case PGASYNC_COPY_BOTH:
			/* 应该无法到达 */
			appendPQExpBufferStr(&fc_conn->errorMessage,
								 "internal error: cannot send pipeline while in COPY\n");
			return 0;
		case PGASYNC_READY:
		case PGASYNC_READY_MORE:
		case PGASYNC_BUSY:
		case PGASYNC_IDLE:
		case PGASYNC_PIPELINE_IDLE:
			/* 可以发送同步 */
			break;
	}

	fc_entry = fc_pqAllocCmdQueueEntry(fc_conn);
	if (fc_entry == NULL)
		return 0;				/* 错误消息已设置 */

	fc_entry->queryclass = PGQUERY_SYNC;
	fc_entry->query = NULL;

	/* 构造同步消息 */
	if (pqPutMsgStart('S', fc_conn) < 0 ||
		pqPutMsgEnd(fc_conn) < 0)
		goto sendFailed;

	/*
	 * 推送数据。在非阻塞模式下，如果我们无法
	 * 发送所有内容，请不要抱怨；PQgetResult() 将执行任何额外的刷新。
	 */
	if (PQflush(fc_conn) < 0)
		goto sendFailed;

	/* 好的，已启动！ */
	fc_pqAppendCmdQueueEntry(fc_conn, fc_entry);

	return 1;

sendFailed:
	fc_pqRecycleCmdQueueEntry(fc_conn, fc_entry);
	/* 错误消息应该已经设置 */
	return 0;
}

/*
 * PQsendFlushRequest
 *		请求服务器刷新其缓冲区。在管道模式下，当不需要同步点时特别有用。
 */
int PQsendFlushRequest(PGconn *fc_conn)
{
	if (!fc_conn)
		return 0;

	/* 如果我们知道没有活动连接，则不要尝试发送。 */
	if (fc_conn->status != CONNECTION_OK)
	{
		appendPQExpBufferStr(&fc_conn->errorMessage,
							 libpq_gettext("no connection to the server\n"));
		return 0;
	}

	/* 如果已经忙着发送，也不能发送，除非排队以便稍后发送 */
	if (fc_conn->asyncStatus != PGASYNC_IDLE &&
		fc_conn->pipelineStatus == PQ_PIPELINE_OFF)
	{
		appendPQExpBufferStr(&fc_conn->errorMessage,
							 libpq_gettext("another command is already in progress\n"));
		return 0;
	}

	if (pqPutMsgStart('H', fc_conn) < 0 ||
		pqPutMsgEnd(fc_conn) < 0)
	{
		return 0;
	}

	/*
	 * 给数据推送 (在管道模式下，仅当我们超过大小阈值时)。在非阻塞模式下，如果我们无法发送所有数据，不要抱怨；PQgetResult()会完成任何额外的刷新。
	 */
	if (fc_pqPipelineFlush(fc_conn) < 0)
		return 0;

	return 1;
}

/* ====== PGresult 的访问函数 ======== */

ExecStatusType
PQresultStatus(const PGresult *fc_res)
{
	if (!fc_res)
		return PGRES_FATAL_ERROR;
	return fc_res->resultStatus;
}

char * PQresStatus(ExecStatusType fc_status)
{
	if ((unsigned int) fc_status >= lengthof(pgresStatus))
		return libpq_gettext("invalid ExecStatusType code");
	return pgresStatus[fc_status];
}

char * PQresultErrorMessage(const PGresult *fc_res)
{
	if (!fc_res || !fc_res->errMsg)
		return "";
	return fc_res->errMsg;
}

char * PQresultVerboseErrorMessage(const PGresult *fc_res,
							PGVerbosity fc_verbosity,
							PGContextVisibility fc_show_context)
{
	PQExpBufferData fc_workBuf;

	/*
	 * 由于调用者需要释放结果字符串，因此我们必须对任何常量结果进行strdup。我们使用普通的strdup，并记录调用者应该在内存不足时预期NULL。
	 */
	if (!fc_res ||
		(fc_res->resultStatus != PGRES_FATAL_ERROR &&
		 fc_res->resultStatus != PGRES_NONFATAL_ERROR))
		return strdup(libpq_gettext("PGresult is not an error result\n"));

	initPQExpBuffer(&fc_workBuf);

	pqBuildErrorMessage3(&fc_workBuf, fc_res, fc_verbosity, fc_show_context);

	/* 如果格式化消息时内存不足，则干净地失败 */
	if (PQExpBufferDataBroken(fc_workBuf))
	{
		termPQExpBuffer(&fc_workBuf);
		return strdup(libpq_gettext("out of memory\n"));
	}

	return fc_workBuf.data;
}

char * PQresultErrorField(const PGresult *fc_res, int fc_fieldcode)
{
	PGMessageField *fc_pfield;

	if (!fc_res)
		return NULL;
	for (fc_pfield = fc_res->errFields; fc_pfield != NULL; fc_pfield = fc_pfield->next)
	{
		if (fc_pfield->code == fc_fieldcode)
			return fc_pfield->contents;
	}
	return NULL;
}

int PQntuples(const PGresult *fc_res)
{
	if (!fc_res)
		return 0;
	return fc_res->ntups;
}

int PQnfields(const PGresult *fc_res)
{
	if (!fc_res)
		return 0;
	return fc_res->numAttributes;
}

int PQbinaryTuples(const PGresult *fc_res)
{
	if (!fc_res)
		return 0;
	return fc_res->binary;
}

/*
 * 辅助例程用于范围检查字段编号和元组编号。
 * 如果正常返回true，如果不正常返回false
 */

static int fc_check_field_number(const PGresult *fc_res, int fc_field_num)
{
	if (!fc_res)
		return false;			/* 没有办法显示错误消息... */
	if (fc_field_num < 0 || fc_field_num >= fc_res->numAttributes)
	{
		pqInternalNotice(&fc_res->noticeHooks,
						 "column number %d is out of range 0..%d",
						 fc_field_num, fc_res->numAttributes - 1);
		return false;
	}
	return true;
}

static int fc_check_tuple_field_number(const PGresult *fc_res,
						 int fc_tup_num, int fc_field_num)
{
	if (!fc_res)
		return false;			/* 没有办法显示错误消息... */
	if (fc_tup_num < 0 || fc_tup_num >= fc_res->ntups)
	{
		pqInternalNotice(&fc_res->noticeHooks,
						 "row number %d is out of range 0..%d",
						 fc_tup_num, fc_res->ntups - 1);
		return false;
	}
	if (fc_field_num < 0 || fc_field_num >= fc_res->numAttributes)
	{
		pqInternalNotice(&fc_res->noticeHooks,
						 "column number %d is out of range 0..%d",
						 fc_field_num, fc_res->numAttributes - 1);
		return false;
	}
	return true;
}

static int fc_check_param_number(const PGresult *fc_res, int fc_param_num)
{
	if (!fc_res)
		return false;			/* 没有办法显示错误消息... */
	if (fc_param_num < 0 || fc_param_num >= fc_res->numParameters)
	{
		pqInternalNotice(&fc_res->noticeHooks,
						 "parameter number %d is out of range 0..%d",
						 fc_param_num, fc_res->numParameters - 1);
		return false;
	}

	return true;
}

/*
 * 如果field_num无效，返回NULL
 */
char * PQfname(const PGresult *fc_res, int fc_field_num)
{
	if (!fc_check_field_number(fc_res, fc_field_num))
		return NULL;
	if (fc_res->attDescs)
		return fc_res->attDescs[fc_field_num].name;
	else
		return NULL;
}

/*
 * PQfnumber: 根据列名查找列号
 *
 * 列名被解析为SQL语句中的形式，包括大小写折叠和双引号处理。但请注意一个潜在的陷阱：前端的下大写可能遵循与后端不同的区域设置规则...
 *
 * 如果没有匹配项，返回-1。在当前后端中，也可能存在多个匹配项，在这种情况下，找到第一个匹配项。
 */
int PQfnumber(const PGresult *fc_res, const char *fc_field_name)
{
	char	   *fc_field_case;
	bool		fc_in_quotes;
	bool		fc_all_lower = true;
	const char *fc_iptr;
	char	   *fc_optr;
	int			fc_i;

	if (!fc_res)
		return -1;

	/*
	 * 注意：拒绝零长度输入字符串是正确的；匹配零长度字段名的正确输入应该是 ""。
	 */
	if (fc_field_name == NULL ||
		fc_field_name[0] == '\0' ||
		fc_res->attDescs == NULL)
		return -1;

	/*
	 * 检查是否可以避免 strdup() 和相关工作，因为传入的字符串在我们进行检查之前不会被更改。
	 */
	for (fc_iptr = fc_field_name; *fc_iptr; fc_iptr++)
	{
		char		fc_c = *fc_iptr;

		if (fc_c == '"' || fc_c != pg_tolower((unsigned char) fc_c))
		{
			fc_all_lower = false;
			break;
		}
	}

	if (fc_all_lower)
		for (fc_i = 0; fc_i < fc_res->numAttributes; fc_i++)
			if (strcmp(fc_field_name, fc_res->attDescs[fc_i].name) == 0)
				return fc_i;

	/* 如果没有成功，则继续进行正常检查。 */

	/*
	 * 注意：这段代码不会拒绝部分引用的字符串，例如
	 * foo"BAR"foo 将变为 fooBARfoo，而这可能应该是一个错误
	 * 条件。
	 */
	fc_field_case = strdup(fc_field_name);
	if (fc_field_case == NULL)
		return -1;				/* 糟糕 */

	fc_in_quotes = false;
	fc_optr = fc_field_case;
	for (fc_iptr = fc_field_case; *fc_iptr; fc_iptr++)
	{
		char		fc_c = *fc_iptr;

		if (fc_in_quotes)
		{
			if (fc_c == '"')
			{
				if (fc_iptr[1] == '"')
				{
					/* 双引号变成单引号 */
					*fc_optr++ = '"';
					fc_iptr++;
				}
				else
					fc_in_quotes = false;
			}
			else
				*fc_optr++ = fc_c;
		}
		else if (fc_c == '"')
			fc_in_quotes = true;
		else
		{
			fc_c = pg_tolower((unsigned char) fc_c);
			*fc_optr++ = fc_c;
		}
	}
	*fc_optr = '\0';

	for (fc_i = 0; fc_i < fc_res->numAttributes; fc_i++)
	{
		if (strcmp(fc_field_case, fc_res->attDescs[fc_i].name) == 0)
		{
			free(fc_field_case);
			return fc_i;
		}
	}
	free(fc_field_case);
	return -1;
}

Oid PQftable(const PGresult *fc_res, int fc_field_num)
{
	if (!fc_check_field_number(fc_res, fc_field_num))
		return InvalidOid;
	if (fc_res->attDescs)
		return fc_res->attDescs[fc_field_num].tableid;
	else
		return InvalidOid;
}

int PQftablecol(const PGresult *fc_res, int fc_field_num)
{
	if (!fc_check_field_number(fc_res, fc_field_num))
		return 0;
	if (fc_res->attDescs)
		return fc_res->attDescs[fc_field_num].columnid;
	else
		return 0;
}

int PQfformat(const PGresult *fc_res, int fc_field_num)
{
	if (!fc_check_field_number(fc_res, fc_field_num))
		return 0;
	if (fc_res->attDescs)
		return fc_res->attDescs[fc_field_num].format;
	else
		return 0;
}

Oid PQftype(const PGresult *fc_res, int fc_field_num)
{
	if (!fc_check_field_number(fc_res, fc_field_num))
		return InvalidOid;
	if (fc_res->attDescs)
		return fc_res->attDescs[fc_field_num].typid;
	else
		return InvalidOid;
}

int PQfsize(const PGresult *fc_res, int fc_field_num)
{
	if (!fc_check_field_number(fc_res, fc_field_num))
		return 0;
	if (fc_res->attDescs)
		return fc_res->attDescs[fc_field_num].typlen;
	else
		return 0;
}

int PQfmod(const PGresult *fc_res, int fc_field_num)
{
	if (!fc_check_field_number(fc_res, fc_field_num))
		return 0;
	if (fc_res->attDescs)
		return fc_res->attDescs[fc_field_num].atttypmod;
	else
		return 0;
}

char * PQcmdStatus(PGresult *fc_res)
{
	if (!fc_res)
		return NULL;
	return fc_res->cmdStatus;
}

/*
 * PQoidStatus -
 *	if 最后一个命令是 INSERT，返回 oid 字符串
 *	if 不是，返回 ""。
 */
char * PQoidStatus(const PGresult *fc_res)
{
	/*
	 * 这必须足够容纳结果。别笑，这比这个函数以前的做法要好。
	 */
	static char fc_buf[24];

	size_t		fc_len;

	if (!fc_res || strncmp(fc_res->cmdStatus, "INSERT ", 7) != 0)
		return "";

	fc_len = strspn(fc_res->cmdStatus + 7, "0123456789");
	if (fc_len > sizeof(fc_buf) - 1)
		fc_len = sizeof(fc_buf) - 1;
	memcpy(fc_buf, fc_res->cmdStatus + 7, fc_len);
	fc_buf[fc_len] = '\0';

	return fc_buf;
}

/*
 * PQoidValue -
 *	一个也许更可取的上述形式，它只是返回
 *	一个 Oid 类型
 */
Oid PQoidValue(const PGresult *fc_res)
{
	char	   *fc_endptr = NULL;
	unsigned long fc_result;

	if (!fc_res ||
		strncmp(fc_res->cmdStatus, "INSERT ", 7) != 0 ||
		fc_res->cmdStatus[7] < '0' ||
		fc_res->cmdStatus[7] > '9')
		return InvalidOid;

	fc_result = strtoul(fc_res->cmdStatus + 7, &fc_endptr, 10);

	if (!fc_endptr || (*fc_endptr != ' ' && *fc_endptr != '\0'))
		return InvalidOid;
	else
		return (Oid) fc_result;
}


/*
 * PQcmdTuples -
 *	如果最后一个命令是 INSERT/UPDATE/DELETE/MERGE/MOVE/FETCH/COPY，
 *	返回一个包含插入/受影响元组数量的字符串。
 *	如果不是，返回 ""。
 *
 *	XXX：这个应该返回一个 int
 */
char * PQcmdTuples(PGresult *fc_res)
{
	char	   *fc_p,
			   *fc_c;

	if (!fc_res)
		return "";

	if (strncmp(fc_res->cmdStatus, "INSERT ", 7) == 0)
	{
		fc_p = fc_res->cmdStatus + 7;
		/* INSERT：跳过 oid 和空格 */
		while (*fc_p && *fc_p != ' ')
			fc_p++;
		if (*fc_p == 0)
			goto interpret_error;	/* 没有空格？ */
		fc_p++;
	}
	else if (strncmp(fc_res->cmdStatus, "SELECT ", 7) == 0 ||
			 strncmp(fc_res->cmdStatus, "DELETE ", 7) == 0 ||
			 strncmp(fc_res->cmdStatus, "UPDATE ", 7) == 0)
		fc_p = fc_res->cmdStatus + 7;
	else if (strncmp(fc_res->cmdStatus, "FETCH ", 6) == 0 ||
			 strncmp(fc_res->cmdStatus, "MERGE ", 6) == 0)
		fc_p = fc_res->cmdStatus + 6;
	else if (strncmp(fc_res->cmdStatus, "MOVE ", 5) == 0 ||
			 strncmp(fc_res->cmdStatus, "COPY ", 5) == 0)
		fc_p = fc_res->cmdStatus + 5;
	else
		return "";

	/* 检查我们是否有一个整数（至少一位数字，没有其他） */
	for (fc_c = fc_p; *fc_c; fc_c++)
	{
		if (!isdigit((unsigned char) *fc_c))
			goto interpret_error;
	}
	if (fc_c == fc_p)
		goto interpret_error;

	return fc_p;

interpret_error:
	pqInternalNotice(&fc_res->noticeHooks,
					 "could not interpret result from server: %s",
					 fc_res->cmdStatus);
	return "";
}

/*
 * PQgetvalue:
 *	返回行 'tup_num' 中字段 'field_num' 的值
 */
char * PQgetvalue(const PGresult *fc_res, int fc_tup_num, int fc_field_num)
{
	if (!fc_check_tuple_field_number(fc_res, fc_tup_num, fc_field_num))
		return NULL;
	return fc_res->tuples[fc_tup_num][fc_field_num].value;
}

/* PQgetlength:
 *	返回字段值的实际字节长度。
 */
int PQgetlength(const PGresult *fc_res, int fc_tup_num, int fc_field_num)
{
	if (!fc_check_tuple_field_number(fc_res, fc_tup_num, fc_field_num))
		return 0;
	if (fc_res->tuples[fc_tup_num][fc_field_num].len != NULL_LEN)
		return fc_res->tuples[fc_tup_num][fc_field_num].len;
	else
		return 0;
}

/* PQgetisnull:
 *	返回字段值的空状态。
 */
int PQgetisnull(const PGresult *fc_res, int fc_tup_num, int fc_field_num)
{
	if (!fc_check_tuple_field_number(fc_res, fc_tup_num, fc_field_num))
		return 1;				/* 假装它是 null */
	if (fc_res->tuples[fc_tup_num][fc_field_num].len == NULL_LEN)
		return 1;
	else
		return 0;
}

/* PQnparams:
 *	返回准备语句的输入参数数量。
 */
int PQnparams(const PGresult *fc_res)
{
	if (!fc_res)
		return 0;
	return fc_res->numParameters;
}

/* PQparamtype:
 *	返回指定语句参数的类型 Oid。
 */
Oid PQparamtype(const PGresult *fc_res, int fc_param_num)
{
	if (!fc_check_param_number(fc_res, fc_param_num))
		return InvalidOid;
	if (fc_res->paramDescs)
		return fc_res->paramDescs[fc_param_num].typid;
	else
		return InvalidOid;
}


/* PQsetnonblocking:
 *	如果参数为 true，则将 PGconn 的数据库连接设置为非阻塞，
 *	如果参数为 false，则将其设置为阻塞，这不会保护
 *	你免受 PQexec() 的影响，只有使用非阻塞 API 时你才会安全。
 *	只需在已连接的数据库连接上调用。
 */
int PQsetnonblocking(PGconn *fc_conn, int fc_arg)
{
	bool		fc_barg;

	if (!fc_conn || fc_conn->status == CONNECTION_BAD)
		return -1;

	fc_barg = (fc_arg ? true : false);

	/* 如果套接字已经处于请求的状态，则提前退出 */
	if (fc_barg == fc_conn->nonblocking)
		return 0;

	/*
	 * 为了保证刷新/查询/结果轮询行为的一致性，我们
	 * 需要在此时刷新发送队列以保证正确的
	 * 行为。这是可以的，因为它们正在进行阻塞模式的
	 * 转换，无论如何我们都可以阻塞它们。
	 *
	 * 清除错误状态，以防 pqFlush 添加到其中，除非我们正在积极
	 * 流水线处理中，在这种情况下，似乎最好不要这样做。
	 */
	if (fc_conn->cmd_queue_head == NULL)
		pqClearConnErrorState(fc_conn);

	/* 如果我们正在从阻塞切换到非阻塞，在这里刷新 */
	if (pqFlush(fc_conn))
		return -1;

	fc_conn->nonblocking = fc_barg;

	return 0;
}

/*
 * 返回数据库连接的阻塞状态
 *		true == 非阻塞，false == 阻塞
 */
int PQisnonblocking(const PGconn *fc_conn)
{
	if (!fc_conn || fc_conn->status == CONNECTION_BAD)
		return false;
	return pqIsnonblocking(fc_conn);
}

/* libpq 是线程安全的吗？ */
int PQisthreadsafe(void)
{
#ifdef ENABLE_THREAD_SAFETY
	return true;
#else
	return false;
#endif
}


/* 尝试强制数据输出，实际上仅对非阻塞用户有用 */
int PQflush(PGconn *fc_conn)
{
	if (!fc_conn || fc_conn->status == CONNECTION_BAD)
		return -1;
	return pqFlush(fc_conn);
}

/*
 * pqPipelineFlush
 *
 * 在管道模式下，数据仅在输出缓冲区达到阈值时刷新。在非管道模式下，它的行为与标准的 pqFlush 一致。
 *
 * 成功时返回 0。
 */
static int fc_pqPipelineFlush(PGconn *fc_conn)
{
	if ((fc_conn->pipelineStatus != PQ_PIPELINE_ON) ||
		(fc_conn->outCount >= OUTBUFFER_THRESHOLD))
		return pqFlush(fc_conn);
	return 0;
}


/*
 *		PQfreemem - 安全释放分配的内存
 *
 * 主要由 Win32 使用，除非是多线程 DLL (/MD 在 VC6)
 * 用于释放 PQescapeBytea()/PQunescapeBytea() 分配的内存。
 */
void PQfreemem(void *fc_ptr)
{
	free(fc_ptr);
}

/*
 * PQfreeNotify - 释放与 PGnotify 关联的内存
 *
 * 此函数仅用于二进制向后兼容性。
 * 新代码应使用 PQfreemem()。一个宏将自动映射
 * 对 PQfreemem 的调用。它将在未来删除。 bjm 2003-03-24
 */

#undef PQfreeNotify
void		PQfreeNotify(PGnotify *fc_notify);

void PQfreeNotify(PGnotify *fc_notify)
{
	PQfreemem(fc_notify);
}


/*
 * 转义任意字符串以获取有效的 SQL 字面量字符串。
 *
 * 将 "'" 替换为 "''"，如果不是标准字符串，将 "\" 替换为 "\\"。
 *
 * length 是源字符串的长度。 （注意：如果更早发现终止 NUL，
 * PQescapeString 会短于 "length" 停止；因此行为类似于 strncpy。）
 *
 * 为了安全，"to" 中的缓冲区必须至少为 2*length + 1 字节长。
 * 输出字符串中添加一个终止的 NUL 字符，无论输入是否以 NUL 终止。
 *
 * 返回输出的实际长度（不计算终止的 NUL）。
 */
static size_t fc_PQescapeStringInternal(PGconn *fc_conn,
					   char *fc_to, const char *fc_from, size_t fc_length,
					   int *fc_error,
					   int fc_encoding, bool fc_std_strings)
{
	const char *fc_source = fc_from;
	char	   *fc_target = fc_to;
	size_t		fc_remaining = strnlen(fc_from, fc_length);

	if (fc_error)
		*fc_error = 0;

	while (fc_remaining > 0)
	{
		char		fc_c = *fc_source;
		int			fc_charlen;
		int			fc_i;

		/* 针对普通 ASCII 的快速路径 */
		if (!IS_HIGHBIT_SET(fc_c))
		{
			/* 如有需要则应用引用 */
			if (SQL_STR_DOUBLE(fc_c, !fc_std_strings))
				*fc_target++ = fc_c;
			/* 复制字符 */
			*fc_target++ = fc_c;
			fc_source++;
			fc_remaining--;
			continue;
		}

		/* 可能多字节字符的慢路径 */
		fc_charlen = pg_encoding_mblen(fc_encoding, fc_source);

		if (fc_remaining < fc_charlen)
		{
			/*
			 * 如果字符的长度超过可用输入，尽可能报告错误，并用无效
			 * 序列替换字符串。无效序列确保转义字符串
			 * 在服务器端触发错误，即使我们不能
			 * 直接在这里报告错误。
			 *
			 * 当我们可以向调用者报告错误时，这并不是 *那么* 关键，
			 * 但如果我们不能，调用者将使用这个字符串
			 * 而不修改，这需要对解析是安全的。
			 *
			 * 我们知道无效序列有足够的空间，因为
			 * "to" 缓冲区需要至少 2 * length + 1 长，且
			 * 最糟糕的情况是我们用两个无效字节替换一个输入字节。
			 */
			if (fc_error)
				*fc_error = 1;
			if (fc_conn)
				appendPQExpBufferStr(&fc_conn->errorMessage,
									 libpq_gettext("incomplete multibyte character\n"));

			pg_encoding_set_invalid(fc_encoding, fc_target);
			fc_target += 2;

			/* 没有更多输入数据，所以我们可以停止 */
			break;
		}
		else if (pg_encoding_verifymbchar(fc_encoding, fc_source, fc_charlen) == -1)
		{
			/*
			 * 多字节字符无效。 验证这一点很重要，因为无效的多字节字符
			 * 可能会用于“跳过”引号字符，例如在逐字符解析时。
			 *
			 * 用无效序列替换与无效字符对应的字节，原因同上。
			 *
			 * 在我们第一次遇到高位集时，验证整个字符串会快一些，
			 * 但这样我们可以仅替换无效字符，这可能使用户更容易
			 * 找到较大字符串中无效编码的部分。
			 */
			if (fc_error)
				*fc_error = 1;
			if (fc_conn)
				appendPQExpBufferStr(&fc_conn->errorMessage,
									 libpq_gettext("invalid multibyte character\n"));

			pg_encoding_set_invalid(fc_encoding, fc_target);
			fc_target += 2;
			fc_remaining -= fc_charlen;

			/*
			 * 复制无效多字节字符后的其余字符串。
			 */
			fc_source += fc_charlen;
		}
		else
		{
			/* 复制字符 */
			for (fc_i = 0; fc_i < fc_charlen; fc_i++)
			{
				*fc_target++ = *fc_source++;
				fc_remaining--;
			}
		}
	}

	/* 写入终止的 NUL 字符。 */
	*fc_target = '\0';

	return fc_target - fc_to;
}

size_t PQescapeStringConn(PGconn *fc_conn,
				   char *fc_to, const char *fc_from, size_t fc_length,
				   int *fc_error)
{
	if (!fc_conn)
	{
		/* 强制空字符串结果 */
		*fc_to = '\0';
		if (fc_error)
			*fc_error = 1;
		return 0;
	}

	if (fc_conn->cmd_queue_head == NULL)
		pqClearConnErrorState(fc_conn);

	return fc_PQescapeStringInternal(fc_conn, fc_to, fc_from, fc_length, fc_error,
								  fc_conn->client_encoding,
								  fc_conn->std_strings);
}

size_t PQescapeString(char *fc_to, const char *fc_from, size_t fc_length)
{
	return fc_PQescapeStringInternal(NULL, fc_to, fc_from, fc_length, NULL,
								  static_client_encoding,
								  static_std_strings);
}


/*
 * 转义任意字符串。如果 as_ident 为真，我们将结果视为标识符进行转义；如果为假，则视为字面量进行转义。结果将返回在一个新分配的缓冲区中。如果因为编码违规或内存不足而失败，我们将返回 NULL，并将错误信息存储到 conn 中。
 */
static char * fc_PQescapeInternal(PGconn *fc_conn, const char *fc_str, size_t fc_len, bool fc_as_ident)
{
	const char *fc_s;
	char	   *fc_result;
	char	   *fc_rp;
	int			fc_num_quotes = 0; /* 根据 as_ident 选择单引号或双引号 */
	int			fc_num_backslashes = 0;
	size_t		fc_input_len = strlen(fc_str);
	size_t		fc_result_size;
	char		fc_quote_char = fc_as_ident ? '"' : '\'';
	bool		fc_validated_mb = false;

	/* 必须有连接，否则立即失败。 */
	if (!fc_conn)
		return NULL;

	if (fc_conn->cmd_queue_head == NULL)
		pqClearConnErrorState(fc_conn);

	/*
	 * 扫描字符串，寻找必须转义的字符和无效编码的数据。
	 */
	fc_s = fc_str;
	for (size_t fc_remaining = fc_input_len; fc_remaining > 0; fc_remaining--, fc_s++)
	{
		if (*fc_s == fc_quote_char)
			++fc_num_quotes;
		else if (*fc_s == '\\')
			++fc_num_backslashes;
		else if (IS_HIGHBIT_SET(*fc_s))
		{
			int			fc_charlen;

			/* 可能多字节字符的慢路径 */
			fc_charlen = pg_encoding_mblen(fc_conn->client_encoding, fc_s);

			if (fc_charlen > fc_remaining)
			{
				appendPQExpBufferStr(&fc_conn->errorMessage,
									 libpq_gettext("incomplete multibyte character\n"));
				return NULL;
			}

			/*
			 * 如果我们还没有检查过，确保多字节字符是有效的。验证无效的多字节字符是重要的，例如，它们可能被用于“跳过”引号字符，比如在逐个解析字符时。
			 *
			 * 当我们第一次遇到任何多字节字符时，我们检查整个字符串剩余部分的有效性。有些编码对较长字符串有优化的实现。
			 */
			if (!fc_validated_mb)
			{
				if (pg_encoding_verifymbstr(fc_conn->client_encoding, fc_s, fc_remaining)
					!= strlen(fc_s))
				{
					appendPQExpBufferStr(&fc_conn->errorMessage,
										 libpq_gettext("invalid multibyte character\n"));
					return NULL;
				}
				fc_validated_mb = true;
			}

			/* 调整 s，考虑到 for 循环会增加它。 */
			fc_s += fc_charlen - 1;
			fc_remaining -= fc_charlen - 1;
		}
	}

	/* 分配输出缓冲区。 */
	fc_result_size = fc_input_len + fc_num_quotes + 3;	/* 两个引号，加上一个 NUL */
	if (!fc_as_ident && fc_num_backslashes > 0)
		fc_result_size += fc_num_backslashes + 2;
	fc_result = fc_rp = (char *) malloc(fc_result_size);
	if (fc_rp == NULL)
	{
		appendPQExpBufferStr(&fc_conn->errorMessage,
							 libpq_gettext("out of memory\n"));
		return NULL;
	}

	/*
	 * 如果我们正在转义包含反斜杠的字面量，则使用 escape 字符串语法，以便在标准符合字符串的任意值下结果都是正确的。在这种情况下，我们还在开头添加一个空格，以防结果可能紧接在标识符后被插入。
	 */
	if (!fc_as_ident && fc_num_backslashes > 0)
	{
		*fc_rp++ = ' ';
		*fc_rp++ = 'E';
	}

	/* 开始引号。 */
	*fc_rp++ = fc_quote_char;

	/*
	 * 如果可能，使用快速路径。
	 *
	 * 我们已经验证了输入字符串在当前编码中是格式良好的。如果它不包含引号，并且在字面逃逸的情况下不包含反斜杠，那么我们可以直接将其复制到输出缓冲区，并添加必要的引号。
	 *
	 * 如果不是，我们必须重新扫描输入并逐个处理每个字符。
	 */
	if (fc_num_quotes == 0 && (fc_num_backslashes == 0 || fc_as_ident))
	{
		memcpy(fc_rp, fc_str, fc_input_len);
		fc_rp += fc_input_len;
	}
	else
	{
		fc_s = fc_str;
		for (size_t fc_remaining = fc_input_len; fc_remaining > 0; fc_remaining--, fc_s++)
		{
			if (*fc_s == fc_quote_char || (!fc_as_ident && *fc_s == '\\'))
			{
				*fc_rp++ = *fc_s;
				*fc_rp++ = *fc_s;
			}
			else if (!IS_HIGHBIT_SET(*fc_s))
				*fc_rp++ = *fc_s;
			else
			{
				int			fc_i = pg_encoding_mblen(fc_conn->client_encoding, fc_s);

				while (1)
				{
					*fc_rp++ = *fc_s;
					if (--fc_i == 0)
						break;
					fc_remaining--;
					++fc_s;		/* for 循环将提供最终增加 */
				}
			}
		}
	}

	/* 结束引号和终止的 NUL。 */
	*fc_rp++ = fc_quote_char;
	*fc_rp = '\0';

	return fc_result;
}

char * PQescapeLiteral(PGconn *fc_conn, const char *fc_str, size_t fc_len)
{
	return fc_PQescapeInternal(fc_conn, fc_str, fc_len, false);
}

char * PQescapeIdentifier(PGconn *fc_conn, const char *fc_str, size_t fc_len)
{
	return fc_PQescapeInternal(fc_conn, fc_str, fc_len, true);
}

/* bytea 的 HEX 编码支持 */
static const char hextbl[] = "0123456789abcdef";

static const int8 hexlookup[128] = {
	-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
	-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
	-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
	0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -1, -1, -1, -1, -1, -1,
	-1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1,
	-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
	-1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1,
	-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
};

static inline char fc_get_hex(char fc_c)
{
	int			fc_res = -1;

	if (fc_c > 0 && fc_c < 127)
		fc_res = hexlookup[(unsigned char) fc_c];

	return (char) fc_res;
}


/*
 *		PQescapeBytea	- 从二进制字符串转换为
 *		在 SQL INSERT 语句中将字符串包含在目标为 bytea 类型的列所需的最小编码。
 *
 *		我们可以使用十六进制或转义（传统）编码。
 *		在转义模式下，应用以下转换：
 *		'\0' == ASCII  0 == \000
 *		'\'' == ASCII 39 == ''
 *		'\\' == ASCII 92 == \\
 *		任何 < 0x20 或 > 0x7e ---> \ooo
 *										（其中 ooo 是八进制表达式）
 *
 *		如果不是 std_strings，所有发送到输出的反斜杠都将被加倍。
 */
static unsigned char *
fc_PQescapeByteaInternal(PGconn *fc_conn,
					  const unsigned char *fc_from, size_t fc_from_length,
					  size_t *fc_to_length, bool fc_std_strings, bool fc_use_hex)
{
	const unsigned char *fc_vp;
	unsigned char *fc_rp;
	unsigned char *fc_result;
	size_t		fc_i;
	size_t		fc_len;
	size_t		fc_bslash_len = (fc_std_strings ? 1 : 2);

	/*
	 * 空字符串有 1 个字符（'\0'）
	 */
	fc_len = 1;

	if (fc_use_hex)
	{
		fc_len += fc_bslash_len + 1 + 2 * fc_from_length;
	}
	else
	{
		fc_vp = fc_from;
		for (fc_i = fc_from_length; fc_i > 0; fc_i--, fc_vp++)
		{
			if (*fc_vp < 0x20 || *fc_vp > 0x7e)
				fc_len += fc_bslash_len + 3;
			else if (*fc_vp == '\'')
				fc_len += 2;
			else if (*fc_vp == '\\')
				fc_len += fc_bslash_len + fc_bslash_len;
			else
				fc_len++;
		}
	}

	*fc_to_length = fc_len;
	fc_rp = fc_result = (unsigned char *) malloc(fc_len);
	if (fc_rp == NULL)
	{
		if (fc_conn)
			appendPQExpBufferStr(&fc_conn->errorMessage,
								 libpq_gettext("out of memory\n"));
		return NULL;
	}

	if (fc_use_hex)
	{
		if (!fc_std_strings)
			*fc_rp++ = '\\';
		*fc_rp++ = '\\';
		*fc_rp++ = 'x';
	}

	fc_vp = fc_from;
	for (fc_i = fc_from_length; fc_i > 0; fc_i--, fc_vp++)
	{
		unsigned char fc_c = *fc_vp;

		if (fc_use_hex)
		{
			*fc_rp++ = hextbl[(fc_c >> 4) & 0xF];
			*fc_rp++ = hextbl[fc_c & 0xF];
		}
		else if (fc_c < 0x20 || fc_c > 0x7e)
		{
			if (!fc_std_strings)
				*fc_rp++ = '\\';
			*fc_rp++ = '\\';
			*fc_rp++ = (fc_c >> 6) + '0';
			*fc_rp++ = ((fc_c >> 3) & 07) + '0';
			*fc_rp++ = (fc_c & 07) + '0';
		}
		else if (fc_c == '\'')
		{
			*fc_rp++ = '\'';
			*fc_rp++ = '\'';
		}
		else if (fc_c == '\\')
		{
			if (!fc_std_strings)
			{
				*fc_rp++ = '\\';
				*fc_rp++ = '\\';
			}
			*fc_rp++ = '\\';
			*fc_rp++ = '\\';
		}
		else
			*fc_rp++ = fc_c;
	}
	*fc_rp = '\0';

	return fc_result;
}

unsigned char * PQescapeByteaConn(PGconn *fc_conn,
				  const unsigned char *fc_from, size_t fc_from_length,
				  size_t *fc_to_length)
{
	if (!fc_conn)
		return NULL;

	if (fc_conn->cmd_queue_head == NULL)
		pqClearConnErrorState(fc_conn);

	return fc_PQescapeByteaInternal(fc_conn, fc_from, fc_from_length, fc_to_length,
								 fc_conn->std_strings,
								 (fc_conn->sversion >= 90000));
}

unsigned char * PQescapeBytea(const unsigned char *fc_from, size_t fc_from_length, size_t *fc_to_length)
{
	return fc_PQescapeByteaInternal(NULL, fc_from, fc_from_length, fc_to_length,
								 static_std_strings,
								 false /* 不能使用十六进制 */ );
}


#define ISFIRSTOCTDIGIT(CH) ((CH) >= '0' && (CH) <= '3')
#define ISOCTDIGIT(CH) ((CH) >= '0' && (CH) <= '7')
#define OCTVAL(CH) ((CH) - '0')

/*
 *		PQunescapeBytea - 将bytea的空终止字符串表示形式
 *		strtext转换为二进制，填充一个缓冲区。它返回一个
 *		指向缓冲区的指针（或在出错时返回NULL），并将缓冲区的
 *		大小存储在retbuflen中。该指针可以随后作为
 *		PQfreemem函数的参数使用。
 *
 *		进行以下转换：
 *		\\	 == ASCII 92 == \
 *		\ooo == 值为ooo的字节（ooo是八进制数字）
 *		\x	 == x（x是未被上述转换匹配的任何字符）
 */
unsigned char * PQunescapeBytea(const unsigned char *fc_strtext, size_t *fc_retbuflen)
{
	size_t		fc_strtextlen,
				fc_buflen;
	unsigned char *fc_buffer,
			   *fc_tmpbuf;
	size_t		fc_i,
				fc_j;

	if (fc_strtext == NULL)
		return NULL;

	fc_strtextlen = strlen((const char *) fc_strtext);

	if (fc_strtext[0] == '\\' && fc_strtext[1] == 'x')
	{
		const unsigned char *fc_s;
		unsigned char *fc_p;

		fc_buflen = (fc_strtextlen - 2) / 2;
		/* 避免不可移植的malloc(0) */
		fc_buffer = (unsigned char *) malloc(fc_buflen > 0 ? fc_buflen : 1);
		if (fc_buffer == NULL)
			return NULL;

		fc_s = fc_strtext + 2;
		fc_p = fc_buffer;
		while (*fc_s)
		{
			char		fc_v1,
						fc_v2;

			/*
			 * 坏输入会被静默忽略。请注意，这包括
			 * 十六进制对之间的空白，这是byteain允许的。
			 */
			fc_v1 = fc_get_hex(*fc_s++);
			if (!*fc_s || fc_v1 == (char) -1)
				continue;
			fc_v2 = fc_get_hex(*fc_s++);
			if (fc_v2 != (char) -1)
				*fc_p++ = (fc_v1 << 4) | fc_v2;
		}

		fc_buflen = fc_p - fc_buffer;
	}
	else
	{
		/*
		 * 输入的长度是输出的最大长度，但加1以避免
		 * 在输入为零长度时引发不可移植的malloc(0)。
		 */
		fc_buffer = (unsigned char *) malloc(fc_strtextlen + 1);
		if (fc_buffer == NULL)
			return NULL;

		for (fc_i = fc_j = 0; fc_i < fc_strtextlen;)
		{
			switch (fc_strtext[fc_i])
			{
				case '\\':
					fc_i++;
					if (fc_strtext[fc_i] == '\\')
						fc_buffer[fc_j++] = fc_strtext[fc_i++];
					else
					{
						if ((ISFIRSTOCTDIGIT(fc_strtext[fc_i])) &&
							(ISOCTDIGIT(fc_strtext[fc_i + 1])) &&
							(ISOCTDIGIT(fc_strtext[fc_i + 2])))
						{
							int			fc_byte;

							fc_byte = OCTVAL(fc_strtext[fc_i++]);
							fc_byte = (fc_byte << 3) + OCTVAL(fc_strtext[fc_i++]);
							fc_byte = (fc_byte << 3) + OCTVAL(fc_strtext[fc_i++]);
							fc_buffer[fc_j++] = fc_byte;
						}
					}

					/*
					 * 注意：如果我们看到'\'后面跟着的不是一个
					 * 识别的转义序列，我们将循环并且没有
					 * 做任何事情，只是推进了i。因此，那个东西将
					 * 在下一个周期作为普通数据输出。边缘
					 * 情况：字符串末尾的'\'将被丢弃。
					 */
					break;

				default:
					fc_buffer[fc_j++] = fc_strtext[fc_i++];
					break;
			}
		}
		fc_buflen = fc_j;				/* buflen是去引号数据的长度 */
	}

	/* 缩小缓冲区大小以避免多余 */
	/* +1可以避免在buflen==0时出现不可移植的行为 */
	fc_tmpbuf = realloc(fc_buffer, fc_buflen + 1);

	/* 只有非常愚蠢的realloc可能失败，但... */
	if (!fc_tmpbuf)
	{
		free(fc_buffer);
		return NULL;
	}

	*fc_retbuflen = fc_buflen;
	return fc_tmpbuf;
}
